diff --git a/NOTICE.md b/NOTICE.md index 79b9a3e..91f9791 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -40,6 +40,12 @@ alibaba/pouch (1.3.0) * Project: https://github.com/alibaba/pouch * Source: https://github.com/alibaba/pouch/releases/tag/1.3.0 +Azure/go-ntlmssp (0.0.0-20221128193559-754e69321358) + +* License: MIT License +* Project: https://github.com/Azure/go-ntlmssp +* Source: https://github.com/Azure/go-ntlmssp/tree/754e69321358ada85ce213a4ec971d3e4d1bfdf7 + armon/go-metrics (0.0.0-20180917152333-f0300d1749da) * License: MIT License @@ -172,6 +178,12 @@ gogo/protobuf (1.3.2) * Project: https://github.com/gogo/protobuf * Source: https://github.com/gogo/protobuf/releases/tag/v1.3.2 +golang-jwt/jwt (4.5.0) + +* License: MIT License +* Project: https://github.com/golang-jwt/jwt +* Source: https://github.com/golang-jwt/jwt/releases/tag/v4.5.0 + golang/mock (1.6.0) * License: Apache License 2.0 @@ -304,6 +316,18 @@ moby/sys/user (0.1.0) * Project: https://github.com/moby/sys/user * Source: https://github.com/moby/sys/tree/user/v0.1.0 +notaryproject/notation-core-go (1.0.1) + +* License: Apache License 2.0 +* Project: https://github.com/notaryproject/notation-core-go +* Source: https://github.com/notaryproject/notation-core-go/releases/tag/v1.0.1 + +notaryproject/notation-go (1.0.1) + +* License: Apache License 2.0 +* Project: https://github.com/notaryproject/notation-go +* Source: https://github.com/notaryproject/notation-go/releases/tag/v1.0.1 + opencontainers/go-digest (1.0.0) * License: Apache License 2.0 @@ -364,11 +388,11 @@ sirupsen/logrus (v1.9.3) * Project: https://github.com/sirupsen/logrus * Source: https://github.com/sirupsen/logrus/releases/tag/v1.9.3 -spf13/cobra (1.2.1) +spf13/cobra (1.7.0) * License: Apache License 2.0 * Project: https://github.com/spf13/cobra -* Source: https://github.com/spf13/cobra/releases/tag/v1.2.1 +* Source: https://github.com/spf13/cobra/releases/tag/v1.7.0 spf13/pflag (1.0.5) @@ -394,6 +418,12 @@ tklauser/numcpus (0.4.0) * Project: https://github.com/tklauser/numcpus * Source: https://github.com/tklauser/numcpus/releases/tag/v0.4.0 +veraison/go-cose (1.1.0) + +* License: Mozilla Public License 2.0 +* Project: https://github.com/veraison/go-cose +* Source: https://github.com/veraison/go-cose/releases/tag/v1.1.0 + vishvananda/netlink (v1.2.1-beta.2) * License: Apache License 2.0 @@ -418,11 +448,11 @@ golang.org/x/net (0.18.0) * Project: https://github.com/golang/net * Source: https://github.com/golang/net/releases/tag/v0.18.0 -golang.org/x/sync (0.3.0) +golang.org/x/sync (0.4.0) * License: BSD 3-Clause "New" or "Revised" License * Project: https://github.com/golang/sync -* Source: https://github.com/golang/sync/releases/tag/v0.3.0 +* Source: https://github.com/golang/sync/releases/tag/v0.4.0 golang.org/x/sys (0.16.0) @@ -442,17 +472,23 @@ golang.org/x/text (0.14.0) * Project: https://github.com/golang/text * Source: https://github.com/golang/text/releases/tag/v0.14.0 +golang.org/x/mod (0.13.0) + +* License: BSD 3-Clause "New" or "Revised" License +* Project: https://github.com/golang/mod +* Source: https://github.com/golang/mod/releases/tag/v0.13.0 + mozilla-services/pkcs7 (0.0.0-20200128120323-432b2356ecb1) * License: MIT License * Project: https://github.com/mozilla-services * Source: https://github.com/mozilla-services/pkcs7/tree/432b2356ecb18209c1cec25680b8a23632794f21 -googleapis/go-genproto (0.0.0-20230410155749-daa745c078e1) +googleapis/go-genproto v0.0.0-20230822172742-b8732ec3820d * License: Apache License 2.0 * Project: https://github.com/googleapis/go-genproto -* Source: https://github.com/googleapis/go-genproto/tree/daa745c078e18def54ea6b63235554b59c97f01d +* Source: https://github.com/googleapis/go-genproto/tree/b8732ec3820d grpc/grpc-go (1.59.0) @@ -484,12 +520,42 @@ go-yaml/yaml (3.0.1) * Project: https://github.com/go-yaml/yaml * Source: https://github.com/go-yaml/yaml/releases/tag/v3.0.1 +oras-project/oras-go (2.3.1) + +* License: Apache License 2.0 +* Project: https://github.com/oras-project/oras-go +* Source: https://github.com/oras-project/oras-go/releases/tag/v2.3.1 + golang/go (1.21.0) * License: BSD 3-Clause "New" or "Revised" License * Project: https://github.com/golang/go * Source: https://github.com/golang/go/releases/tag/go1.21.0 +x448/float16 (v0.8.4) + +* License: MIT License +* Project: https://github.com/x448/float16 +* Source: https://github.com/x448/float16/releases/tag/v0.8.4 + +go-ldap/ldap (v3.4.6) + +* License: MIT License +* Project: https://github.com/go-ldap/ldap +* Source: https://github.com/go-ldap/ldap/releases/tag/v3.4.6 + +fxamacker/cbor/v2 v2.5.0 + +* License: MIT License +* Project: https://github.com/fxamacker/cbor +* Source: https://github.com/fxamacker/cbor/releases/tag/v2.5.0 + +go-asn1-ber/asn1-ber v1.5.5 + +* License: MIT License +* Project: https://github.com/go-asn1-ber/asn1-ber +* Source: https://github.com/go-asn1-ber/asn1-ber/releases/tag/v1.5.5 + ## Cryptography Content may contain encryption software. The country in which you are currently diff --git a/containerm/api/services/containers/containers.pb.go b/containerm/api/services/containers/containers.pb.go index 3c11d11..60cd59b 100644 --- a/containerm/api/services/containers/containers.pb.go +++ b/containerm/api/services/containers/containers.pb.go @@ -12,7 +12,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.29.0 +// protoc-gen-go v1.32.0 // protoc v4.22.0 // source: api/services/containers/containers.proto @@ -920,7 +920,8 @@ type RemoveContainerRequest struct { Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // Whether the container should be removed disregarding its current state. - Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"` + Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"` + StopOptions *containers.StopOptions `protobuf:"bytes,3,opt,name=stopOptions,proto3" json:"stopOptions,omitempty"` } func (x *RemoveContainerRequest) Reset() { @@ -969,6 +970,13 @@ func (x *RemoveContainerRequest) GetForce() bool { return false } +func (x *RemoveContainerRequest) GetStopOptions() *containers.StopOptions { + if x != nil { + return x.StopOptions + } + return nil +} + type GetLogsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1200,177 +1208,185 @@ var file_api_services_containers_containers_proto_rawDesc = []byte{ 0x0a, 0x16, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3e, 0x0a, 0x16, - 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x34, 0x0a, 0x0e, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x61, - 0x69, 0x6c, 0x22, 0x23, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x32, 0x99, 0x13, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0xdd, 0x01, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, - 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x69, 0x2e, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, - 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xd4, 0x01, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xbc, 0x01, 0x0a, + 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, 0x7c, 0x0a, + 0x0b, 0x73, 0x74, 0x6f, 0x70, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, + 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0b, + 0x73, 0x74, 0x6f, 0x70, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x34, 0x0a, 0x0e, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, + 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x74, 0x61, 0x69, + 0x6c, 0x22, 0x23, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x32, 0x99, 0x13, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0xdd, 0x01, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, + 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x69, 0x2e, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, + 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xd4, 0x01, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x65, 0x2e, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, + 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, + 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x66, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xd9, 0x01, 0x0a, + 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x73, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x66, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, - 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xd9, 0x01, - 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, - 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, - 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xdf, 0x01, 0x0a, 0x0a, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xdf, 0x01, 0x0a, 0x0a, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, + 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x66, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, + 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x88, 0x01, 0x0a, 0x05, 0x53, + 0x74, 0x61, 0x72, 0x74, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0xe1, 0x01, 0x0a, 0x06, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, + 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, + 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x69, 0x2e, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, + 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x41, 0x74, + 0x74, 0x61, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x86, 0x01, 0x0a, 0x04, 0x53, 0x74, + 0x6f, 0x70, 0x12, 0x66, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, + 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x8a, 0x01, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x68, 0x2e, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, + 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x8c, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x69, 0x2e, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, + 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x52, + 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x88, + 0x01, 0x0a, 0x05, 0x50, 0x61, 0x75, 0x73, 0x65, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x66, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, - 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x88, 0x01, 0x0a, 0x05, - 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x50, 0x61, 0x75, 0x73, + 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x8c, 0x01, 0x0a, 0x07, 0x55, 0x6e, + 0x70, 0x61, 0x75, 0x73, 0x65, 0x12, 0x69, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0xe1, 0x01, 0x0a, 0x06, 0x41, 0x74, 0x74, 0x61, 0x63, - 0x68, 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, - 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x69, 0x2e, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, - 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x41, - 0x74, 0x74, 0x61, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x86, 0x01, 0x0a, 0x04, 0x53, - 0x74, 0x6f, 0x70, 0x12, 0x66, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x8a, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x8a, 0x01, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x68, - 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, - 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x12, 0x8c, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x69, 0x2e, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, - 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, - 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x88, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x75, 0x73, 0x65, 0x12, 0x67, 0x2e, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, - 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x50, 0x61, 0x75, - 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x8c, 0x01, 0x0a, 0x07, 0x55, - 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, 0x12, 0x69, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, - 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, - 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x8a, 0x01, 0x0a, 0x06, 0x52, 0x65, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x8a, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, - 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0xcd, 0x01, 0x0a, 0x04, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x60, 0x2e, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, - 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x61, - 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, - 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x30, 0x01, 0x42, 0x5d, 0x5a, 0x5b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x2d, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2f, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x3b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x8a, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, + 0x12, 0x68, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, + 0x6c, 0x69, 0x70, 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0xcd, 0x01, 0x0a, 0x04, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x60, 0x2e, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, + 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x2e, 0x47, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x61, 0x2e, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x63, 0x6c, 0x69, 0x70, + 0x73, 0x65, 0x5f, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, + 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x30, 0x01, 0x42, 0x5d, 0x5a, 0x5b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x65, 0x63, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x2d, 0x6b, 0x61, 0x6e, 0x74, 0x6f, 0x2f, 0x63, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6d, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x3b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1419,39 +1435,40 @@ var file_api_services_containers_containers_proto_depIdxs = []int32{ 19, // 4: github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainerMessage.container:type_name -> github.com.eclipse_kanto.container_management.containerm.api.types.containers.Container 20, // 5: github.com.eclipse_kanto.container_management.containerm.api.services.containers.StopContainerRequest.stopOptions:type_name -> github.com.eclipse_kanto.container_management.containerm.api.types.containers.StopOptions 21, // 6: github.com.eclipse_kanto.container_management.containerm.api.services.containers.UpdateContainerRequest.updateOptions:type_name -> github.com.eclipse_kanto.container_management.containerm.api.types.containers.UpdateOptions - 1, // 7: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Create:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.CreateContainerRequest - 3, // 8: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Get:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetContainerRequest - 0, // 9: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.List:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainersRequest - 0, // 10: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.ListStream:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainersRequest - 7, // 11: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Start:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.StartContainerRequest - 8, // 12: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Attach:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.AttachContainerRequest - 10, // 13: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Stop:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.StopContainerRequest - 11, // 14: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Update:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.UpdateContainerRequest - 12, // 15: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Restart:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.RestartContainerRequest - 13, // 16: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Pause:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.PauseContainerRequest - 14, // 17: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Unpause:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.UnpauseContainerRequest - 15, // 18: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Rename:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.RenameContainerRequest - 16, // 19: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Remove:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.RemoveContainerRequest - 17, // 20: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Logs:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetLogsRequest - 2, // 21: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Create:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.CreateContainerResponse - 4, // 22: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Get:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetContainerResponse - 5, // 23: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.List:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainersResponse - 6, // 24: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.ListStream:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainerMessage - 22, // 25: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Start:output_type -> google.protobuf.Empty - 9, // 26: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Attach:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.AttachContainerResponse - 22, // 27: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Stop:output_type -> google.protobuf.Empty - 22, // 28: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Update:output_type -> google.protobuf.Empty - 22, // 29: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Restart:output_type -> google.protobuf.Empty - 22, // 30: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Pause:output_type -> google.protobuf.Empty - 22, // 31: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Unpause:output_type -> google.protobuf.Empty - 22, // 32: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Rename:output_type -> google.protobuf.Empty - 22, // 33: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Remove:output_type -> google.protobuf.Empty - 18, // 34: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Logs:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetLogsResponse - 21, // [21:35] is the sub-list for method output_type - 7, // [7:21] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 20, // 7: github.com.eclipse_kanto.container_management.containerm.api.services.containers.RemoveContainerRequest.stopOptions:type_name -> github.com.eclipse_kanto.container_management.containerm.api.types.containers.StopOptions + 1, // 8: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Create:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.CreateContainerRequest + 3, // 9: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Get:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetContainerRequest + 0, // 10: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.List:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainersRequest + 0, // 11: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.ListStream:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainersRequest + 7, // 12: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Start:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.StartContainerRequest + 8, // 13: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Attach:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.AttachContainerRequest + 10, // 14: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Stop:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.StopContainerRequest + 11, // 15: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Update:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.UpdateContainerRequest + 12, // 16: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Restart:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.RestartContainerRequest + 13, // 17: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Pause:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.PauseContainerRequest + 14, // 18: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Unpause:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.UnpauseContainerRequest + 15, // 19: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Rename:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.RenameContainerRequest + 16, // 20: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Remove:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.RemoveContainerRequest + 17, // 21: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Logs:input_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetLogsRequest + 2, // 22: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Create:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.CreateContainerResponse + 4, // 23: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Get:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetContainerResponse + 5, // 24: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.List:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainersResponse + 6, // 25: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.ListStream:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.ListContainerMessage + 22, // 26: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Start:output_type -> google.protobuf.Empty + 9, // 27: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Attach:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.AttachContainerResponse + 22, // 28: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Stop:output_type -> google.protobuf.Empty + 22, // 29: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Update:output_type -> google.protobuf.Empty + 22, // 30: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Restart:output_type -> google.protobuf.Empty + 22, // 31: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Pause:output_type -> google.protobuf.Empty + 22, // 32: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Unpause:output_type -> google.protobuf.Empty + 22, // 33: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Rename:output_type -> google.protobuf.Empty + 22, // 34: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Remove:output_type -> google.protobuf.Empty + 18, // 35: github.com.eclipse_kanto.container_management.containerm.api.services.containers.Containers.Logs:output_type -> github.com.eclipse_kanto.container_management.containerm.api.services.containers.GetLogsResponse + 22, // [22:36] is the sub-list for method output_type + 8, // [8:22] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_api_services_containers_containers_proto_init() } diff --git a/containerm/api/services/containers/containers.proto b/containerm/api/services/containers/containers.proto index 8703756..fbd8854 100644 --- a/containerm/api/services/containers/containers.proto +++ b/containerm/api/services/containers/containers.proto @@ -169,6 +169,7 @@ message RemoveContainerRequest { // Whether the container should be removed disregarding its current state. bool force = 2; + github.com.eclipse_kanto.container_management.containerm.api.types.containers.StopOptions stopOptions = 3; } message GetLogsRequest { diff --git a/containerm/cli/cli_command_ctrs_create.go b/containerm/cli/cli_command_ctrs_create.go index 00985a5..914120a 100644 --- a/containerm/cli/cli_command_ctrs_create.go +++ b/containerm/cli/cli_command_ctrs_create.go @@ -14,13 +14,16 @@ package main import ( "context" + "encoding/json" "fmt" + "os" "time" "github.com/eclipse-kanto/container-management/containerm/containers/types" "github.com/eclipse-kanto/container-management/containerm/log" "github.com/eclipse-kanto/container-management/containerm/util" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type createCmd struct { @@ -46,6 +49,7 @@ type createConfig struct { interactive bool privileged bool network string + containerFile string extraHosts []string extraCapabilities []string devices []string @@ -68,10 +72,10 @@ type createConfig struct { func (cc *createCmd) init(cli *cli) { cc.cli = cli cc.cmd = &cobra.Command{ - Use: "create [option]... container-image-id [command] [command-arg]...", + Use: "create [option]... [container-image-id] [command] [command-arg]...", Short: "Create a container.", Long: "Create a container.", - Args: cobra.MinimumNArgs(1), + Args: cobra.MinimumNArgs(0), RunE: func(cmd *cobra.Command, args []string) error { return cc.run(args) }, @@ -81,37 +85,60 @@ func (cc *createCmd) init(cli *cli) { cc.setupFlags() } -func (cc *createCmd) run(args []string) error { - // parse parameters - imageID := args[0] +func initContainer(config createConfig, imageName string) *types.Container { + return &types.Container{ + Name: config.name, + Image: types.Image{ + Name: imageName, + }, + HostConfig: &types.HostConfig{ + Privileged: config.privileged, + ExtraHosts: config.extraHosts, + ExtraCapabilities: config.extraCapabilities, + NetworkMode: types.NetworkMode(config.network), + }, + IOConfig: &types.IOConfig{ + Tty: config.terminal, + OpenStdin: config.interactive, + }, + } +} + +func (cc *createCmd) containerFromFile() (*types.Container, error) { + var err error + cc.cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) { + if flag.Changed && flag.Name != "file" { + err = log.NewError("no other flags are expected when creating a container from file") + } + }) + if err != nil { + return nil, err + } + byteValue, err := os.ReadFile(cc.config.containerFile) + if err != nil { + return nil, err + } + ctrToCreate := initContainer(cc.config, "") + if err = json.Unmarshal(byteValue, ctrToCreate); err != nil { + return nil, err + } + return ctrToCreate, nil +} + +func (cc *createCmd) containerFromFlags(args []string) (*types.Container, error) { + ctrToCreate := initContainer(cc.config, args[0]) + var command []string if len(args) > 1 { command = args[1:] } if cc.config.privileged && cc.config.devices != nil { - return log.NewError("cannot create the container as privileged and with specified devices at the same time - choose one of the options") + return nil, log.NewError("cannot create the container as privileged and with specified devices at the same time - choose one of the options") } if cc.config.privileged && cc.config.extraCapabilities != nil { - return log.NewError("cannot create the container as privileged and with extra capabilities at the same time - choose one of the options") - } - - ctrToCreate := &types.Container{ - Name: cc.config.name, - Image: types.Image{ - Name: imageID, - }, - HostConfig: &types.HostConfig{ - Privileged: cc.config.privileged, - ExtraHosts: cc.config.extraHosts, - ExtraCapabilities: cc.config.extraCapabilities, - NetworkMode: types.NetworkMode(cc.config.network), - }, - IOConfig: &types.IOConfig{ - Tty: cc.config.terminal, - OpenStdin: cc.config.interactive, - }, + return nil, log.NewError("cannot create the container as privileged and with extra capabilities at the same time - choose one of the options") } if cc.config.env != nil || command != nil { @@ -124,7 +151,7 @@ func (cc *createCmd) run(args []string) error { if cc.config.devices != nil { devs, err := util.ParseDeviceMappings(cc.config.devices) if err != nil { - return err + return nil, err } ctrToCreate.HostConfig.Devices = devs } @@ -132,7 +159,7 @@ func (cc *createCmd) run(args []string) error { if cc.config.mountPoints != nil { mounts, err := util.ParseMountPoints(cc.config.mountPoints) if err != nil { - return err + return nil, err } else if mounts != nil { ctrToCreate.Mounts = mounts } @@ -140,7 +167,7 @@ func (cc *createCmd) run(args []string) error { if cc.config.ports != nil { mappings, err := util.ParsePortMappings(cc.config.ports) if err != nil { - return err + return nil, err } ctrToCreate.HostConfig.PortMappings = mappings } @@ -155,7 +182,6 @@ func (cc *createCmd) run(args []string) error { Type: types.No, } case string(types.UnlessStopped): - ctrToCreate.HostConfig.RestartPolicy = &types.RestartPolicy{ Type: types.UnlessStopped, } @@ -203,7 +229,31 @@ func (cc *createCmd) run(args []string) error { ctrToCreate.HostConfig.Resources = getResourceLimits(cc.config.resources) ctrToCreate.Image.DecryptConfig = getDecryptConfig(cc.config) - if err := util.ValidateContainer(ctrToCreate); err != nil { + return ctrToCreate, nil +} + +func (cc *createCmd) run(args []string) error { + var ( + ctrToCreate *types.Container + err error + ) + + if len(cc.config.containerFile) > 0 { + if len(args) > 0 { + return log.NewError("no arguments are expected when creating a container from file") + } + if ctrToCreate, err = cc.containerFromFile(); err != nil { + return err + } + } else if len(args) != 0 { + if ctrToCreate, err = cc.containerFromFlags(args); err != nil { + return err + } + } else { + return log.NewError("container image argument is expected") + } + + if err = util.ValidateContainer(ctrToCreate); err != nil { return err } @@ -314,4 +364,5 @@ func (cc *createCmd) setupFlags() { flagSet.StringSliceVar(&cc.config.decRecipients, "dec-recipients", nil, "Sets a recipients certificates list of the image (used only for PKCS7 and must be an x509)") //init extra capabilities flagSet.StringSliceVar(&cc.config.extraCapabilities, "cap-add", nil, "Add Linux capabilities to the container") + flagSet.StringVarP(&cc.config.containerFile, "file", "f", "", "Creates a container with a predefined config given by the user.") } diff --git a/containerm/cli/cli_command_ctrs_create_test.go b/containerm/cli/cli_command_ctrs_create_test.go index 2242b29..d361606 100644 --- a/containerm/cli/cli_command_ctrs_create_test.go +++ b/containerm/cli/cli_command_ctrs_create_test.go @@ -14,6 +14,8 @@ package main import ( "context" + "encoding/json" + "os" "strconv" "strings" "testing" @@ -30,6 +32,7 @@ const ( createCmdFlagTerminal = "t" createCmdFlagInteractive = "i" createCmdFlagPrivileged = "privileged" + createCmdFlagContainerFile = "file" createCmdFlagRestartPolicy = "rp" createCmdFlagRestartPolicyMaxCount = "rp-cnt" createCmdFlagRestartPolicyTimeout = "rp-to" @@ -74,10 +77,11 @@ func TestCreateCmdSetupFlags(t *testing.T) { createCliTest.init() expectedCfg := createConfig{ - name: "", - terminal: true, - interactive: true, - privileged: true, + name: "", + terminal: true, + interactive: true, + privileged: true, + containerFile: string("config.json"), restartPolicy: restartPolicy{ kind: string(types.Always), timeout: 10, @@ -109,6 +113,7 @@ func TestCreateCmdSetupFlags(t *testing.T) { createCmdFlagTerminal: strconv.FormatBool(expectedCfg.terminal), createCmdFlagInteractive: strconv.FormatBool(expectedCfg.interactive), createCmdFlagPrivileged: strconv.FormatBool(expectedCfg.privileged), + createCmdFlagContainerFile: expectedCfg.containerFile, createCmdFlagRestartPolicy: expectedCfg.restartPolicy.kind, createCmdFlagRestartPolicyMaxCount: strconv.Itoa(expectedCfg.restartPolicy.maxRetryCount), createCmdFlagRestartPolicyTimeout: strconv.FormatInt(expectedCfg.restartPolicy.timeout, 10), @@ -451,6 +456,14 @@ func (createTc *createCommandTest) generateRunExecutionConfigs() map[string]test }, mockExecution: createTc.mockExecCreateWithExtraCapabilities, }, + "test_create_extra_capabilities_with_privileged": { + args: createCmdArgs, + flags: map[string]string{ + createCmdFlagExtraCapabilities: "CAP_NET_ADMIN", + createCmdFlagPrivileged: "true", + }, + mockExecution: createTc.mockExecCreateWithExtraCapabilitiesWithPrivileged, + }, // Test privileged "test_create_privileged": { args: createCmdArgs, @@ -459,6 +472,35 @@ func (createTc *createCommandTest) generateRunExecutionConfigs() map[string]test }, mockExecution: createTc.mockExecCreateWithPrivileged, }, + // Test container file + "test_create_no_args": { + mockExecution: createTc.mockExecCreateWithNoArgs, + }, + "test_create_container_file": { + flags: map[string]string{ + createCmdFlagContainerFile: "../pkg/testutil/config/container/valid.json", + }, + mockExecution: createTc.mockExecCreateContainerFile, + }, + "test_create_container_file_invalid_path": { + flags: map[string]string{ + createCmdFlagContainerFile: "/test/test", + }, + mockExecution: createTc.mockExecCreateContainerFileInvalidPath, + }, + "test_create_container_file_invalid_json": { + flags: map[string]string{ + createCmdFlagContainerFile: "../pkg/testutil/config/container/invalid.json", + }, + mockExecution: createTc.mockExecCreateContainerFileInvalidJSON, + }, + "test_create_container_file_with_args": { + args: createCmdArgs, + flags: map[string]string{ + createCmdFlagContainerFile: "../pkg/testutil/config/container/valid.json", + }, + mockExecution: createTc.mockExecCreateContainerFileWithArgs, + }, // Test terminal "test_create_terminal": { args: createCmdArgs, @@ -884,6 +926,12 @@ func (createTc *createCommandTest) mockExecCreateWithPortsProto(args []string) e createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Eq(container)).Times(1).Return(container, nil) return nil } + +func (createTc *createCommandTest) mockExecCreateWithNoArgs(args []string) error { + createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0) + return log.NewError("container image argument is expected") +} + func (createTc *createCommandTest) mockExecCreateWithPortsIncorrectPortsConfig(args []string) error { createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0) return log.NewError("Incorrect port mapping configuration") @@ -937,6 +985,11 @@ func (createTc *createCommandTest) mockExecCreateWithExtraCapabilities(args []st return nil } +func (createTc *createCommandTest) mockExecCreateWithExtraCapabilitiesWithPrivileged(args []string) error { + createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0) + return log.NewError("cannot create the container as privileged and with extra capabilities at the same time - choose one of the options") +} + func (createTc *createCommandTest) mockExecCreateWithPrivileged(args []string) error { container := initExpectedCtr(&types.Container{ Image: types.Image{ @@ -950,6 +1003,39 @@ func (createTc *createCommandTest) mockExecCreateWithPrivileged(args []string) e createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Eq(container)).Times(1).Return(container, nil) return nil } + +func (createTc *createCommandTest) mockExecCreateContainerFile(_ []string) error { + byteValue, _ := os.ReadFile("../pkg/testutil/config/container/valid.json") + container := &types.Container{ + HostConfig: &types.HostConfig{ + NetworkMode: types.NetworkModeBridge, + }, + IOConfig: &types.IOConfig{}, + } + json.Unmarshal(byteValue, container) + + createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Eq(container)).Times(1).Return(container, nil) + return nil +} + +func (createTc *createCommandTest) mockExecCreateContainerFileInvalidPath(_ []string) error { + createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0) + _, err := os.ReadFile("/test/test") + return err +} + +func (createTc *createCommandTest) mockExecCreateContainerFileInvalidJSON(_ []string) error { + createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0) + byteValue, _ := os.ReadFile("../pkg/testutil/config/container/invalid.json") + err := json.Unmarshal(byteValue, &types.Container{}) + return err +} + +func (createTc *createCommandTest) mockExecCreateContainerFileWithArgs(_ []string) error { + createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0) + return log.NewError("no arguments are expected when creating a container from file") +} + func (createTc *createCommandTest) mockExecCreateWithTerminal(args []string) error { container := initExpectedCtr(&types.Container{ Image: types.Image{ @@ -1061,19 +1147,13 @@ func initExpectedCtr(ctr *types.Container) *types.Container { //merge default and provided if ctr.HostConfig == nil { ctr.HostConfig = &types.HostConfig{ - Privileged: false, - ExtraHosts: nil, - ExtraCapabilities: nil, - NetworkMode: types.NetworkModeBridge, + NetworkMode: types.NetworkModeBridge, } } else if ctr.HostConfig.NetworkMode == "" { ctr.HostConfig.NetworkMode = types.NetworkModeBridge } if ctr.IOConfig == nil { - ctr.IOConfig = &types.IOConfig{ - Tty: false, - OpenStdin: false, - } + ctr.IOConfig = &types.IOConfig{} } if ctr.HostConfig.LogConfig == nil { ctr.HostConfig.LogConfig = &types.LogConfiguration{ diff --git a/containerm/cli/cli_command_ctrs_list.go b/containerm/cli/cli_command_ctrs_list.go index 878f490..fea1705 100644 --- a/containerm/cli/cli_command_ctrs_list.go +++ b/containerm/cli/cli_command_ctrs_list.go @@ -17,6 +17,7 @@ import ( "fmt" "os" "strconv" + "strings" "text/tabwriter" "github.com/eclipse-kanto/container-management/containerm/client" @@ -30,7 +31,9 @@ type listCmd struct { } type listConfig struct { - name string + name string + quiet bool + filter []string } func (cc *listCmd) init(cli *cli) { @@ -43,7 +46,7 @@ func (cc *listCmd) init(cli *cli) { RunE: func(cmd *cobra.Command, args []string) error { return cc.run(args) }, - Example: "list\n list --name \n list -n ", + Example: " list\n list --name \n list --quiet\n list --filter status=created", } cc.setupFlags() } @@ -57,8 +60,25 @@ func (cc *listCmd) run(args []string) error { if err != nil { return err } + if len(cc.config.filter) > 0 { + filtered, err := filterBy(cc.config.filter, ctrs) + if err != nil { + return err + } + ctrs = filtered + } + if cc.config.quiet { + for i, ctr := range ctrs { + if i != len(ctrs)-1 { + fmt.Printf("%s ", ctr.ID) + } else { + fmt.Println(ctr.ID) + } + } + return nil + } if len(ctrs) == 0 { - fmt.Println("No found containers.") + fmt.Println("No containers found.") } else { prettyPrint(ctrs) } @@ -69,6 +89,45 @@ func (cc *listCmd) setupFlags() { flagSet := cc.cmd.Flags() // init name flags flagSet.StringVarP(&cc.config.name, "name", "n", "", "List all containers with a specific name.") + flagSet.BoolVarP(&cc.config.quiet, "quiet", "q", false, "List only container IDs.") + flagSet.StringSliceVar(&cc.config.filter, "filter", nil, "Lists only containers with a specified filter. The containers can be filtered by their status, image and exitcode.") +} + +func filterBy(input []string, ctrs []*types.Container) ([]*types.Container, error) { + var ( + holderStatus string + holderImage string + convertedExitCode int = -1 + err error + ) + filteredCtrs := []*types.Container{} + for _, inp := range input { + if strings.HasPrefix(inp, "status=") { + holderStatus = strings.TrimPrefix(inp, "status=") + } else if strings.HasPrefix(inp, "image=") { + holderImage = strings.TrimPrefix(inp, "image=") + } else if strings.HasPrefix(inp, "exitcode=") { + convertedExitCode, err = strconv.Atoi(strings.TrimPrefix(inp, "exitcode=")) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("no filter: %s", strings.Split(inp, "=")[0]) + } + } + for _, ctr := range ctrs { + if holderStatus != "" && !strings.EqualFold(ctr.State.Status.String(), holderStatus) { + continue + } + if holderImage != "" && !strings.EqualFold(ctr.Image.Name, holderImage) { + continue + } + if int64(convertedExitCode) != -1 && ctr.State.ExitCode != int64(convertedExitCode) { + continue + } + filteredCtrs = append(filteredCtrs, ctr) + } + return filteredCtrs, nil } /* diff --git a/containerm/cli/cli_command_ctrs_list_test.go b/containerm/cli/cli_command_ctrs_list_test.go index 883162a..ee95de3 100644 --- a/containerm/cli/cli_command_ctrs_list_test.go +++ b/containerm/cli/cli_command_ctrs_list_test.go @@ -14,6 +14,7 @@ package main import ( "context" + "fmt" "testing" "github.com/eclipse-kanto/container-management/containerm/client" @@ -24,7 +25,9 @@ import ( const ( // command flags - listCmdFlagName = "name" + listCmdFlagName = "name" + listCmdFlagQuiet = "quiet" + listCmdFlagFilter = "filter" // test input constants listContainerID = "test-ctr" @@ -117,6 +120,42 @@ func (listTc *listCommandTest) generateRunExecutionConfigs() map[string]testRunE }, mockExecution: listTc.mockExecListByNameNoCtrs, }, + "test_list_quiet": { + flags: map[string]string{ + listCmdFlagQuiet: "true", + }, + mockExecution: listTc.mockExecListQuiet, + }, + "test_list_with_filter_status": { + flags: map[string]string{ + listCmdFlagFilter: "status=creating", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_filter_image": { + flags: map[string]string{ + listCmdFlagFilter: "image=test", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_filter_exit_code": { + flags: map[string]string{ + listCmdFlagFilter: "exitcode=0", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_multiple_filters": { + flags: map[string]string{ + listCmdFlagFilter: "image=test,exitcode=0", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_filter_error": { + flags: map[string]string{ + listCmdFlagFilter: "test=test", + }, + mockExecution: listTc.mockExecListWithFilterError, + }, "test_list_by_name_err": { flags: map[string]string{ listCmdFlagName: listFlagName, @@ -172,6 +211,48 @@ func (listTc *listCommandTest) mockExecListByNameNoCtrs(args []string) error { return nil } +func (listTc *listCommandTest) mockExecListQuiet(args []string) error { + // setup expected calls + ctrs := []*types.Container{ + { + ID: fmt.Sprintf("%s-%d", listContainerID, 1), + Name: listFlagName, + State: &types.State{}, + }, + { + ID: fmt.Sprintf("%s-%d", listContainerID, 2), + Name: listFlagName, + State: &types.State{}, + }, + } + listTc.mockClient.EXPECT().List(context.Background()).Times(1).Return(ctrs, nil) + // no error expected + return nil +} + +func (listTc *listCommandTest) mockExecListWithFilter(args []string) error { + // setup expected calls + ctrs := []*types.Container{{ + ID: listContainerID, + Name: listFlagName, + State: &types.State{}, + }} + listTc.mockClient.EXPECT().List(context.Background()).Times(1).Return(ctrs, nil) + // no error expected + return nil +} + +func (listTc *listCommandTest) mockExecListWithFilterError(args []string) error { + err := log.NewError("no filter: test") + ctrs := []*types.Container{{ + ID: listContainerID, + Name: listFlagName, + State: &types.State{}, + }} + listTc.mockClient.EXPECT().List(context.Background()).Times(1).Return(ctrs, nil) + return err +} + func (listTc *listCommandTest) mockExecListByNameErrors(args []string) error { // setup expected calls err := log.NewError("failed to get containers list") diff --git a/containerm/cli/cli_command_ctrs_remove.go b/containerm/cli/cli_command_ctrs_remove.go index 8981e3c..87b80c6 100644 --- a/containerm/cli/cli_command_ctrs_remove.go +++ b/containerm/cli/cli_command_ctrs_remove.go @@ -14,9 +14,11 @@ package main import ( "context" + "errors" "github.com/eclipse-kanto/container-management/containerm/containers/types" utilcli "github.com/eclipse-kanto/container-management/containerm/util/cli" + errorutil "github.com/eclipse-kanto/container-management/containerm/util/error" "github.com/spf13/cobra" ) @@ -26,37 +28,60 @@ type removeCmd struct { } type removeConfig struct { - force bool - name string + force bool + name string + timeout string } func (cc *removeCmd) init(cli *cli) { cc.cli = cli cc.cmd = &cobra.Command{ - Use: "remove ", - Short: "Remove a container.", - Long: "Remove a container and frees the associated resources.", - Args: cobra.MaximumNArgs(1), + Use: "remove ...", + Short: "Remove one or more containers.", + Long: "Remove one or more containers and frees the associated resources.", RunE: func(cmd *cobra.Command, args []string) error { return cc.run(args) }, - Example: "remove \n remove --name \n remove -n ", + Example: " remove \n remove \n remove --name \n remove -n ", } cc.setupFlags() } func (cc *removeCmd) run(args []string) error { var ( - ctr *types.Container - err error - ctx = context.Background() + ctr *types.Container + err error + ctx = context.Background() + errs errorutil.CompoundError + stopOpts *types.StopOpts ) - // parse parameters - if ctr, err = utilcli.ValidateContainerByNameArgsSingle(ctx, args, cc.config.name, cc.cli.gwManClient); err != nil { - return err + if cc.config.force && cc.config.timeout != "" { + stopOpts = &types.StopOpts{Force: true} + if stopOpts.Timeout, err = durationStringToSeconds(cc.config.timeout); err != nil { + return err + } + } + if len(args) == 0 { + if ctr, err = utilcli.ValidateContainerByNameArgsSingle(ctx, nil, cc.config.name, cc.cli.gwManClient); err != nil { + return err + } + return cc.cli.gwManClient.Remove(ctx, ctr.ID, cc.config.force, stopOpts) + } + for _, arg := range args { + ctr, err = utilcli.ValidateContainerByNameArgsSingle(ctx, []string{arg}, cc.config.name, cc.cli.gwManClient) + if err == nil { + if err = cc.cli.gwManClient.Remove(ctx, ctr.ID, cc.config.force, stopOpts); err != nil { + errs.Append(err) + } + } else { + errs.Append(err) + } + } + if errs.Size() > 0 { + return errors.New(errs.ErrorWithMessage("containers couldn't be removed due to the following reasons: ")) } - return cc.cli.gwManClient.Remove(ctx, ctr.ID, cc.config.force) + return nil } func (cc *removeCmd) setupFlags() { @@ -65,4 +90,5 @@ func (cc *removeCmd) setupFlags() { flagSet.BoolVarP(&cc.config.force, "force", "f", false, "Force stopping before removing a container") // init name flags flagSet.StringVarP(&cc.config.name, "name", "n", "", "Remove a container with a specific name.") + flagSet.StringVarP(&cc.config.timeout, "time", "t", "", "Sets the timeout period to gracefully stop the container as duration string, e.g. 15s or 1m15s. When timeout expires the container process would be forcibly killed. If not specified the daemon default container stop timeout will be used.") } diff --git a/containerm/cli/cli_command_ctrs_remove_test.go b/containerm/cli/cli_command_ctrs_remove_test.go index 3959661..f625ba8 100644 --- a/containerm/cli/cli_command_ctrs_remove_test.go +++ b/containerm/cli/cli_command_ctrs_remove_test.go @@ -21,13 +21,15 @@ import ( "github.com/eclipse-kanto/container-management/containerm/client" "github.com/eclipse-kanto/container-management/containerm/containers/types" "github.com/eclipse-kanto/container-management/containerm/log" + errorutil "github.com/eclipse-kanto/container-management/containerm/util/error" "github.com/golang/mock/gomock" ) const ( // command flags - removeCmdFlagForce = "force" - removeCmdFlagName = "name" + removeCmdFlagForce = "force" + removeCmdFlagName = "name" + removeCmdFlagTimeout = "time" // test input constants removeContainerID = "test-ctr" @@ -52,12 +54,14 @@ func TestRemoveCmdSetupFlags(t *testing.T) { rmCliTest.init() expectedCfg := removeConfig{ - force: true, - name: removeContainerName, + force: true, + name: removeContainerName, + timeout: "50", } flagsToApply := map[string]string{ - removeCmdFlagForce: strconv.FormatBool(expectedCfg.force), - removeCmdFlagName: expectedCfg.name, + removeCmdFlagForce: strconv.FormatBool(expectedCfg.force), + removeCmdFlagName: expectedCfg.name, + removeCmdFlagTimeout: expectedCfg.timeout, } execTestSetupFlags(t, rmCliTest, flagsToApply, expectedCfg) @@ -86,8 +90,9 @@ func (rmTc *removeCommandTest) commandConfig() interface{} { func (rmTc *removeCommandTest) commandConfigDefault() interface{} { return removeConfig{ - force: false, - name: "", + force: false, + name: "", + timeout: "", } } @@ -121,6 +126,14 @@ func (rmTc *removeCommandTest) generateRunExecutionConfigs() map[string]testRunE args: removeCmdArgs, mockExecution: rmTc.mockExecRemoveNoErrors, }, + "test_remove_multiple": { + args: []string{"test-container-one", "test-container-two"}, + mockExecution: rmTc.mockExecRemoveMultipleNoErrors, + }, + "test_remove_multiple_with_err": { + args: []string{"test-container-one", "test-container-two"}, + mockExecution: rmTc.mockExecRemoveMultipleWithErrors, + }, "test_remove_by_id_err": { args: removeCmdArgs, mockExecution: rmTc.mockExecRemoveGetError, @@ -167,6 +180,22 @@ func (rmTc *removeCommandTest) generateRunExecutionConfigs() map[string]testRunE flags: map[string]string{removeCmdFlagForce: "true"}, mockExecution: rmTc.mockExecForceRemoveError, }, + "test_remove_with_timeout": { + args: stopCmdArgs, + flags: map[string]string{ + removeCmdFlagForce: "true", + stopCmdFlagTimeout: "20s", + }, + mockExecution: rmTc.mockExecRemoveWithTimeout, + }, + "test_remove_with_timeout_round": { + args: stopCmdArgs, + flags: map[string]string{ + removeCmdFlagForce: "true", + stopCmdFlagTimeout: "19.7s", + }, + mockExecution: rmTc.mockExecRemoveWithTimeout, + }, } } @@ -174,15 +203,17 @@ func (rmTc *removeCommandTest) generateRunExecutionConfigs() map[string]testRunE func (rmTc *removeCommandTest) mockExecRemoveIDAndName(args []string) error { rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(0) rmTc.mockClient.EXPECT().List(context.Background(), gomock.Any()).Times(0) - rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false).Times(0) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false, nil).Times(0) return log.NewError("Container ID and --name (-n) cannot be provided at the same time - use only one of them") } + func (rmTc *removeCommandTest) mockExecRemoveNoIDorName(args []string) error { rmTc.mockClient.EXPECT().Get(context.Background(), gomock.Any()).Times(0) rmTc.mockClient.EXPECT().List(context.Background(), gomock.Any()).Times(0) rmTc.mockClient.EXPECT().Get(context.Background(), gomock.Any()).Times(0) return log.NewError("You must provide either an ID or a name for the container via --name (-n) ") } + func (rmTc *removeCommandTest) mockExecRemoveNoErrors(args []string) error { // setup expected calls ctr := &types.Container{ @@ -190,11 +221,46 @@ func (rmTc *removeCommandTest) mockExecRemoveNoErrors(args []string) error { Name: removeContainerName, } rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(ctr, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false).Times(1).Return(nil) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false, nil).Times(1).Return(nil) + // no error expected + return nil +} + +func (rmTc *removeCommandTest) mockExecRemoveMultipleNoErrors(args []string) error { + // setup expected calls + ctr1 := &types.Container{ + ID: args[0], + Name: removeContainerName, + } + ctr2 := &types.Container{ + ID: args[1], + Name: removeContainerName, + } + rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(ctr1, nil) + rmTc.mockClient.EXPECT().Get(context.Background(), args[1]).Times(1).Return(ctr2, nil) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false, nil).Times(1).Return(nil) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[1], false, nil).Times(1).Return(nil) // no error expected return nil } +func (rmTc *removeCommandTest) mockExecRemoveMultipleWithErrors(args []string) error { + // setup expected calls + var errs errorutil.CompoundError + err1 := log.NewErrorf("The requested container with ID = %s was not found.", args[0]) + err2 := errors.New("failed to remove container") + ctr := &types.Container{ + ID: args[1], + Name: removeContainerName, + } + rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(nil, err1) + rmTc.mockClient.EXPECT().Get(context.Background(), args[1]).Times(1).Return(ctr, nil) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[1], false, nil).Times(1).Return(err2) + errs.Append(err1, err2) + + return errors.New(errs.ErrorWithMessage("containers couldn't be removed due to the following reasons: ")) +} + func (rmTc *removeCommandTest) mockExecRemoveError(args []string) error { // setup expected calls err := errors.New("failed to remove container") @@ -203,14 +269,15 @@ func (rmTc *removeCommandTest) mockExecRemoveError(args []string) error { Name: removeContainerName, } rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(ctr, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false).Times(1).Return(err) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false, nil).Times(1).Return(err) return err } + func (rmTc *removeCommandTest) mockExecRemoveByNameNoErrors(args []string) error { // setup expected calls ctrs := []*types.Container{{ID: removeContainerID, Name: removeContainerName}} rmTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(removeContainerName))).Times(1).Return(ctrs, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), ctrs[0].ID, false).Times(1).Return(nil) + rmTc.mockClient.EXPECT().Remove(context.Background(), ctrs[0].ID, false, nil).Times(1).Return(nil) // no error expected return nil } @@ -220,46 +287,52 @@ func (rmTc *removeCommandTest) mockExecRemoveByNameError(args []string) error { err := errors.New("failed to remove container") ctrs := []*types.Container{{ID: removeContainerID, Name: removeContainerName}} rmTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(removeContainerName))).Times(1).Return(ctrs, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), ctrs[0].ID, false).Times(1).Return(err) + rmTc.mockClient.EXPECT().Remove(context.Background(), ctrs[0].ID, false, nil).Times(1).Return(err) // no error expected return err } + func (rmTc *removeCommandTest) mockExecRemoveByNameListError(args []string) error { // setup expected calls err := errors.New("failed to list containers") rmTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(removeContainerName))).Times(1).Return(nil, err) - rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false).Times(0) + rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false, nil).Times(0) return err } + func (rmTc *removeCommandTest) mockExecRemoveByNameListNilCtrs(args []string) error { // setup expected calls rmTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(removeContainerName))).Times(1).Return(nil, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false).Times(0) + rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false, nil).Times(0) return log.NewErrorf("The requested container with name = %s was not found. Try using an ID instead.", removeContainerName) } + func (rmTc *removeCommandTest) mockExecRemoveByNameListZeroCtrs(args []string) error { // setup expected calls rmTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(removeContainerName))).Times(1).Return([]*types.Container{}, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false).Times(0) + rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false, nil).Times(0) return log.NewErrorf("The requested container with name = %s was not found. Try using an ID instead.", removeContainerName) } + func (rmTc *removeCommandTest) mockExecRemoveByNameListMoreThanOneCtrs(args []string) error { // setup expected calls rmTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(removeContainerName))).Times(1).Return([]*types.Container{{}, {}}, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false).Times(0) + rmTc.mockClient.EXPECT().Remove(context.Background(), gomock.Any(), false, nil).Times(0) return log.NewErrorf("There are more than one containers with name = %s. Try using an ID instead.", removeContainerName) } + func (rmTc *removeCommandTest) mockExecRemoveGetNilError(args []string) error { // setup expected calls rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(nil, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false).Times(0) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false, nil).Times(0) return log.NewErrorf("The requested container with ID = %s was not found.", args[0]) } + func (rmTc *removeCommandTest) mockExecRemoveGetError(args []string) error { // setup expected calls err := errors.New("failed to remove container") rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(nil, err) - rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false).Times(0) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], false, nil).Times(0) return err } @@ -270,7 +343,7 @@ func (rmTc *removeCommandTest) mockExecForceRemoveNoErrors(args []string) error Name: removeContainerName, } rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(ctr, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], true).Times(1).Return(nil) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], true, nil).Times(1).Return(nil) // no error expected return nil } @@ -283,7 +356,16 @@ func (rmTc *removeCommandTest) mockExecForceRemoveError(args []string) error { Name: removeContainerName, } rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(ctr, nil) - rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], true).Times(1).Return(err) + rmTc.mockClient.EXPECT().Remove(context.Background(), args[0], true, nil).Times(1).Return(err) // no error expected return err } + +func (rmTc *removeCommandTest) mockExecRemoveWithTimeout(args []string) error { + ctr := &types.Container{ + ID: args[0], + } + rmTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(ctr, nil) + rmTc.mockClient.EXPECT().Remove(context.Background(), ctr.ID, true, &types.StopOpts{Timeout: 20, Force: true}).Times(1).Return(nil) + return nil +} diff --git a/containerm/cli/cli_command_ctrs_stop.go b/containerm/cli/cli_command_ctrs_stop.go index 3a4336f..2fbf94c 100644 --- a/containerm/cli/cli_command_ctrs_stop.go +++ b/containerm/cli/cli_command_ctrs_stop.go @@ -14,7 +14,7 @@ package main import ( "context" - "math" + "time" "github.com/eclipse-kanto/container-management/containerm/containers/types" "github.com/eclipse-kanto/container-management/containerm/util" @@ -28,7 +28,7 @@ type stopCmd struct { } type stopConfig struct { - timeout int64 + timeout string name string force bool signal string @@ -44,7 +44,7 @@ func (cc *stopCmd) init(cli *cli) { RunE: func(cmd *cobra.Command, args []string) error { return cc.run(args) }, - Example: "stop \n stop --name \n stop -n ", + Example: " stop \n stop --name \n stop -n ", } cc.setupFlags() } @@ -63,8 +63,8 @@ func (cc *stopCmd) run(args []string) error { Force: cc.config.force, Signal: cc.config.signal, } - if cc.config.timeout != math.MinInt64 { - stopOpts.Timeout = cc.config.timeout + if stopOpts.Timeout, err = durationStringToSeconds(cc.config.timeout); err != nil { + return err } if err = util.ValidateStopOpts(stopOpts); err != nil { return err @@ -72,10 +72,21 @@ func (cc *stopCmd) run(args []string) error { return cc.cli.gwManClient.Stop(ctx, container.ID, stopOpts) } +func durationStringToSeconds(duration string) (int64, error) { + if duration == "" { + return 0, nil + } + stopTime, err := time.ParseDuration(duration) + if err != nil { + return 0, err + } + return int64(stopTime.Round(time.Second).Seconds()), nil +} + func (cc *stopCmd) setupFlags() { flagSet := cc.cmd.Flags() // init timeout flag - flagSet.Int64VarP(&cc.config.timeout, "timeout", "t", math.MinInt64, "Sets the timeout period in seconds to gracefully stop the container. When timeout expires the container process would be forcibly killed.") + flagSet.StringVarP(&cc.config.timeout, "time", "t", "", "Sets the timeout period to gracefully stop the container as duration string, e.g. 15s or 1m15s. When timeout expires the container process would be forcibly killed. If not specified the daemon default container stop timeout will be used.") // init name flag flagSet.StringVarP(&cc.config.name, "name", "n", "", "Stop a container with a specific name.") // init force flag diff --git a/containerm/cli/cli_command_ctrs_stop_test.go b/containerm/cli/cli_command_ctrs_stop_test.go index 9c17e95..cbcb9b7 100644 --- a/containerm/cli/cli_command_ctrs_stop_test.go +++ b/containerm/cli/cli_command_ctrs_stop_test.go @@ -14,7 +14,6 @@ package main import ( "context" - "math" "strconv" "testing" @@ -26,7 +25,7 @@ import ( const ( // command flags - stopCmdFlagTimeout = "timeout" + stopCmdFlagTimeout = "time" stopCmdFlagName = "name" stopCmdFlagForce = "force" stopCmdFlagSignal = "signal" @@ -65,14 +64,16 @@ func TestStopCmdFlags(t *testing.T) { stopCliTest.init() expectedCfg := stopConfig{ - timeout: 50, + timeout: "50", name: stopContainerName, force: true, signal: sigterm, } + conv, _ := strconv.ParseInt(expectedCfg.timeout, 10, 64) + flagsToApply := map[string]string{ - stopCmdFlagTimeout: strconv.FormatInt(expectedCfg.timeout, 10), + stopCmdFlagTimeout: strconv.FormatInt(conv, 10), stopCmdFlagName: expectedCfg.name, stopCmdFlagForce: strconv.FormatBool(expectedCfg.force), } @@ -103,7 +104,7 @@ func (stopTc *stopCommandTest) commandConfig() interface{} { func (stopTc *stopCommandTest) commandConfigDefault() interface{} { return stopConfig{ - timeout: math.MinInt64, + timeout: "", name: "", force: false, signal: sigterm, @@ -175,7 +176,7 @@ func (stopTc *stopCommandTest) generateRunExecutionConfigs() map[string]testRunE "test_stop_with_timeout": { args: stopCmdArgs, flags: map[string]string{ - stopCmdFlagTimeout: "20", + stopCmdFlagTimeout: "20s", }, mockExecution: stopTc.mockExecStopWithTimeout, }, @@ -186,7 +187,7 @@ func (stopTc *stopCommandTest) generateRunExecutionConfigs() map[string]testRunE "test_stop_with_negative_timeout_error": { args: stopCmdArgs, flags: map[string]string{ - stopCmdFlagTimeout: "-10", + stopCmdFlagTimeout: "-10s", }, mockExecution: stopTc.mockExecStopWithNegativeTimeoutError, }, @@ -198,7 +199,7 @@ func (stopTc *stopCommandTest) generateRunExecutionConfigs() map[string]testRunE }, "test_stop_by_name_with_timeout": { flags: map[string]string{ - stopCmdFlagTimeout: "20", + stopCmdFlagTimeout: "20s", stopCmdFlagName: stopContainerName, }, mockExecution: stopTc.mockExecStopByNameWithTimeout, @@ -211,7 +212,7 @@ func (stopTc *stopCommandTest) generateRunExecutionConfigs() map[string]testRunE }, "test_stop_by_name_with_negative_timeout_error": { flags: map[string]string{ - stopCmdFlagTimeout: "-10", + stopCmdFlagTimeout: "-10s", stopCmdFlagName: stopContainerName, }, mockExecution: stopTc.mockExecStopByNameWithNegativeTimeoutError, @@ -275,6 +276,7 @@ func (stopTc *stopCommandTest) mockExecStopNoIDorName(args []string) error { stopTc.mockClient.EXPECT().Stop(context.Background(), gomock.Any(), gomock.Any()).Times(0) return log.NewError("You must provide either an ID or a name for the container via --name (-n) ") } + func (stopTc *stopCommandTest) mockExecStopByIDGetErr(args []string) error { err := log.NewError("could not get container") stopTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(nil, err) @@ -282,6 +284,7 @@ func (stopTc *stopCommandTest) mockExecStopByIDGetErr(args []string) error { stopTc.mockClient.EXPECT().Stop(context.Background(), gomock.Any(), gomock.Any()).Times(0) return err } + func (stopTc *stopCommandTest) mockExecStopByIDGetNilCtr(args []string) error { stopTc.mockClient.EXPECT().Get(context.Background(), args[0]).Times(1).Return(nil, nil) stopTc.mockClient.EXPECT().List(context.Background(), gomock.Any()).Times(0) @@ -296,24 +299,28 @@ func (stopTc *stopCommandTest) mockExecStopByNameListErr(args []string) error { stopTc.mockClient.EXPECT().Stop(context.Background(), gomock.Any(), gomock.Any()).Times(0) return err } + func (stopTc *stopCommandTest) mockExecStopByNameListNilCtrs(args []string) error { stopTc.mockClient.EXPECT().Get(context.Background(), gomock.Any()).Times(0) stopTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(startContainerName))).Times(1).Return(nil, nil) stopTc.mockClient.EXPECT().Stop(context.Background(), gomock.Any(), gomock.Any()).Times(0) return log.NewErrorf("The requested container with name = %s was not found. Try using an ID instead.", startContainerName) } + func (stopTc *stopCommandTest) mockExecStopByNameListZeroCtrs(args []string) error { stopTc.mockClient.EXPECT().Get(context.Background(), gomock.Any()).Times(0) stopTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(startContainerName))).Times(1).Return([]*types.Container{}, nil) stopTc.mockClient.EXPECT().Stop(context.Background(), gomock.Any(), gomock.Any()).Times(0) return log.NewErrorf("The requested container with name = %s was not found. Try using an ID instead.", startContainerName) } + func (stopTc *stopCommandTest) mockExecStopByNameListMoreThanOneCtrs(args []string) error { stopTc.mockClient.EXPECT().Get(context.Background(), gomock.Any()).Times(0) stopTc.mockClient.EXPECT().List(context.Background(), gomock.AssignableToTypeOf(client.WithName(startContainerName))).Times(1).Return([]*types.Container{{}, {}}, nil) stopTc.mockClient.EXPECT().Stop(context.Background(), gomock.Any(), gomock.Any()).Times(0) return log.NewErrorf("There are more than one containers with name = %s. Try using an ID instead.", startContainerName) } + func (stopTc *stopCommandTest) mockExecStopNoErrors(args []string) error { // setup expected calls ctr := &types.Container{ @@ -345,6 +352,7 @@ func (stopTc *stopCommandTest) mockExecStopError(args []string) error { stopTc.mockClient.EXPECT().Stop(context.Background(), ctr.ID, defaultStopOpts).Times(1).Return(err) return err } + func (stopTc *stopCommandTest) mockExecStopWithNegativeTimeoutError(args []string) error { err := log.NewError("the timeout = -10 shouldn't be negative") ctr := &types.Container{ @@ -365,6 +373,7 @@ func (stopTc *stopCommandTest) mockExecStopByNameWithNegativeTimeoutError(args [ stopTc.mockClient.EXPECT().Stop(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) return err } + func (stopTc *stopCommandTest) mockExecStopByNameNoErrors(args []string) error { // setup expected calls ctrs := []*types.Container{{ diff --git a/containerm/client/client.go b/containerm/client/client.go index 361d61a..2baeb19 100644 --- a/containerm/client/client.go +++ b/containerm/client/client.go @@ -149,8 +149,8 @@ func (cl *client) Rename(ctx context.Context, id string, name string) error { } // Remove removes a container, it may be running or stopped and so on. -func (cl *client) Remove(ctx context.Context, id string, force bool) error { - _, err := cl.grpcContainersClient.Remove(ctx, &pbcontainers.RemoveContainerRequest{Id: id, Force: force}) +func (cl *client) Remove(ctx context.Context, id string, force bool, stopOpts *types.StopOpts) error { + _, err := cl.grpcContainersClient.Remove(ctx, &pbcontainers.RemoveContainerRequest{Id: id, Force: force, StopOptions: protobuf.ToProtoStopOptions(stopOpts)}) return err } diff --git a/containerm/client/client_api.go b/containerm/client/client_api.go index 9fd3590..b130ee0 100644 --- a/containerm/client/client_api.go +++ b/containerm/client/client_api.go @@ -59,7 +59,7 @@ type Client interface { Rename(ctx context.Context, id string, name string) error // Remove removes a container, it may be running or stopped and so on. - Remove(ctx context.Context, id string, force bool) error + Remove(ctx context.Context, id string, force bool, stopOpts *types.StopOpts) error ProjectInfo(ctx context.Context) (sysinfotypes.ProjectInfo, error) diff --git a/containerm/client/client_test.go b/containerm/client/client_test.go index b726f13..ccd393d 100644 --- a/containerm/client/client_test.go +++ b/containerm/client/client_test.go @@ -605,9 +605,10 @@ func TestUpdate(t *testing.T) { } type testRemoveArgs struct { - ctx context.Context - id string - force bool + ctx context.Context + id string + force bool + stopOpts *types.StopOpts } type mockExecRemove func(args testRemoveArgs) error @@ -634,6 +635,14 @@ func TestRemove(t *testing.T) { }, mockExecution: mockExecRemoveErrors, }, + "test_remove_stop_opts": { + args: testRemoveArgs{ + ctx: testCtx, + force: true, + stopOpts: &types.StopOpts{Timeout: 10}, + }, + mockExecution: mockExecRemoveStopOpts, + }, } // execute tests @@ -643,7 +652,7 @@ func TestRemove(t *testing.T) { expectedRunErr := testCase.mockExecution(testCase.args) - resultErr := testClient.Remove(testCase.args.ctx, testCase.args.id, testCase.args.force) + resultErr := testClient.Remove(testCase.args.ctx, testCase.args.id, testCase.args.force, testCase.args.stopOpts) testutil.AssertError(t, expectedRunErr, resultErr) }) @@ -998,6 +1007,18 @@ func mockExecRemoveNoErrors(args testRemoveArgs) error { return nil } +func mockExecRemoveStopOpts(args testRemoveArgs) error { + mockContainersClient.EXPECT().Remove(args.ctx, gomock.Eq(&pbcontainers.RemoveContainerRequest{ + Id: args.id, + Force: args.force, + StopOptions: &containers.StopOptions{ + Timeout: args.stopOpts.Timeout, + Force: args.stopOpts.Force, + }, + })).Times(1).Return(nil, nil) + return nil +} + func mockExecRemoveErrors(args testRemoveArgs) error { err := errors.New("failed to remove") mockContainersClient.EXPECT().Remove(args.ctx, gomock.Eq(&pbcontainers.RemoveContainerRequest{ diff --git a/containerm/ctr/ctr_client_opts.go b/containerm/ctr/ctr_client_opts.go index 01e7926..8570411 100644 --- a/containerm/ctr/ctr_client_opts.go +++ b/containerm/ctr/ctr_client_opts.go @@ -13,26 +13,29 @@ package ctr import ( + "time" + "github.com/eclipse-kanto/container-management/containerm/containers/types" "github.com/eclipse-kanto/container-management/containerm/log" - "time" ) // ContainerOpts represents container engine client's configuration options. type ContainerOpts func(ctrOptions *ctrOpts) error type ctrOpts struct { - namespace string - connectionPath string - registryConfigs map[string]*RegistryConfig - rootExec string - metaPath string - imageDecKeys []string - imageDecRecipients []string - runcRuntime types.Runtime - imageExpiry time.Duration - imageExpiryDisable bool - leaseID string + namespace string + connectionPath string + registryConfigs map[string]*RegistryConfig + rootExec string + metaPath string + imageDecKeys []string + imageDecRecipients []string + runcRuntime types.Runtime + imageExpiry time.Duration + imageExpiryDisable bool + leaseID string + imageVerifierType VerifierType + imageVerifierConfig map[string]string } // RegistryConfig represents a single registry's access configuration. @@ -156,3 +159,24 @@ func WithCtrdLeaseID(leaseID string) ContainerOpts { return nil } } + +// WithCtrImageVerifierType sets the image verifier type of the container client instance. +func WithCtrImageVerifierType(imageVerifierType string) ContainerOpts { + return func(ctrOptions *ctrOpts) error { + switch imageVerifierType { + case string(VerifierNone), string(VerifierNotation): + ctrOptions.imageVerifierType = VerifierType(imageVerifierType) + default: + return log.NewErrorf("unexpected image verifier type = %s", imageVerifierType) + } + return nil + } +} + +// WithCtrImageVerifierConfig sets the image verifier config of the container client instance. +func WithCtrImageVerifierConfig(imageVerifierConfig map[string]string) ContainerOpts { + return func(ctrOptions *ctrOpts) error { + ctrOptions.imageVerifierConfig = imageVerifierConfig + return nil + } +} diff --git a/containerm/ctr/ctr_client_opts_test.go b/containerm/ctr/ctr_client_opts_test.go index f5c76c8..61d4d37 100644 --- a/containerm/ctr/ctr_client_opts_test.go +++ b/containerm/ctr/ctr_client_opts_test.go @@ -13,11 +13,12 @@ package ctr import ( - "github.com/eclipse-kanto/container-management/containerm/containers/types" - "github.com/eclipse-kanto/container-management/containerm/log" "testing" "time" + "github.com/eclipse-kanto/container-management/containerm/containers/types" + "github.com/eclipse-kanto/container-management/containerm/log" + "github.com/eclipse-kanto/container-management/containerm/pkg/testutil" ) @@ -35,7 +36,7 @@ const ( ) var ( - regConfig = &RegistryConfig{ + testRegConfig = &RegistryConfig{ IsInsecure: false, Credentials: &AuthCredentials{ UserID: testUser, @@ -43,19 +44,22 @@ var ( }, Transport: nil, } + testVerifierConfig = map[string]string{"testKey": "testValue", "testAnotherKey": "testAnotherValue"} testOpt = &ctrOpts{ - namespace: testNamespace, - connectionPath: testConnectionPath, - registryConfigs: map[string]*RegistryConfig{testHost: regConfig}, - rootExec: testRootExec, - metaPath: testMetaPath, - imageDecKeys: testDecKeys, - imageDecRecipients: testDecRecipients, - runcRuntime: types.RuntimeTypeV2runcV2, - imageExpiry: testImageExpiry, - imageExpiryDisable: testImageExpiryDisable, - leaseID: testLeaseID, + namespace: testNamespace, + connectionPath: testConnectionPath, + registryConfigs: map[string]*RegistryConfig{testHost: testRegConfig}, + rootExec: testRootExec, + metaPath: testMetaPath, + imageDecKeys: testDecKeys, + imageDecRecipients: testDecRecipients, + runcRuntime: types.RuntimeTypeV2runcV2, + imageExpiry: testImageExpiry, + imageExpiryDisable: testImageExpiryDisable, + leaseID: testLeaseID, + imageVerifierType: VerifierNotation, + imageVerifierConfig: testVerifierConfig, } ) @@ -72,18 +76,27 @@ func TestCtrOpts(t *testing.T) { expectedOpts: &ctrOpts{}, expectedErr: log.NewErrorf("unexpected runc runtime = unknown"), }, + "test_ctr_opts_unexpected_image_verifier_type_error": { + opts: []ContainerOpts{ + WithCtrImageVerifierType("unknown"), + }, + expectedOpts: &ctrOpts{}, + expectedErr: log.NewErrorf("unexpected image verifier type = unknown"), + }, "test_ctr_opts_no_error": { opts: []ContainerOpts{WithCtrdConnectionPath(testConnectionPath), WithCtrdNamespace(testNamespace), WithCtrdRootExec(testRootExec), WithCtrdMetaPath(testMetaPath), - WithCtrdRegistryConfigs(map[string]*RegistryConfig{testHost: regConfig}), + WithCtrdRegistryConfigs(map[string]*RegistryConfig{testHost: testRegConfig}), WithCtrdImageDecryptKeys(testDecKeys...), WithCtrdImageDecryptRecipients(testDecRecipients...), WithCtrdRuncRuntime(string(types.RuntimeTypeV2runcV2)), WithCtrdImageExpiry(testImageExpiry), WithCtrdImageExpiryDisable(testImageExpiryDisable), - WithCtrdLeaseID(testLeaseID)}, + WithCtrdLeaseID(testLeaseID), + WithCtrImageVerifierType(string(VerifierNotation)), + WithCtrImageVerifierConfig(testVerifierConfig)}, expectedOpts: testOpt, }, } diff --git a/containerm/ctr/ctr_tls_util.go b/containerm/ctr/ctr_tls_util.go index 0cd099b..028d3f0 100644 --- a/containerm/ctr/ctr_tls_util.go +++ b/containerm/ctr/ctr_tls_util.go @@ -16,6 +16,8 @@ import ( "crypto/tls" "crypto/x509" "io/ioutil" + "net" + "net/http" "path/filepath" "runtime" @@ -111,6 +113,33 @@ func validateTLSConfigFile(file, expectedFileExt string) error { return nil } +func getTransport(isInsecure bool, config *TLSConfig, host string) *http.Transport { + tlsConfig := createDefaultTLSConfig(isInsecure) + if !isInsecure && config != nil { + if err := applyLocalTLSConfig(config, tlsConfig); err != nil { + log.WarnErr(err, "could not process provided TLS configuration - default will be used for registry host %s", host) + tlsConfig = createDefaultTLSConfig(isInsecure) + } else { + log.Debug("successfully applied TLS configuration for registry host %s", host) + } + } + + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: registryResolverDialContextTimeout, + KeepAlive: registryResolverDialContextKeepAlive, + DualStack: true, + }).DialContext, + MaxIdleConns: registryResolverTransportMaxIdeConns, + IdleConnTimeout: registryResolverTransportIdleConnTimeout, + TLSHandshakeTimeout: registryResolverTransportTLSHandshakeTimeout, + TLSClientConfig: tlsConfig, + ExpectContinueTimeout: registryResolverTransportExpectContinueTimeout, + } + return tr +} + // excludes cipher suites with security issues func supportedCipherSuites() []uint16 { cs := tls.CipherSuites() diff --git a/containerm/ctr/ctr_verifier.go b/containerm/ctr/ctr_verifier.go new file mode 100644 index 0000000..ed248de --- /dev/null +++ b/containerm/ctr/ctr_verifier.go @@ -0,0 +1,53 @@ +// Copyright (c) 2023 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +package ctr + +import ( + "context" + + "github.com/eclipse-kanto/container-management/containerm/containers/types" + "github.com/eclipse-kanto/container-management/containerm/log" +) + +const ( + // VerifierNone is a VerifierType denoting that no verification will be performed + VerifierNone = VerifierType("none") + // VerifierNotation is a VerifierType denoting that verification will be performed with notation + VerifierNotation = VerifierType("notation") + notationKeyConfigDir = "configDir" + notationKeyLibexecDir = "libexecDir" +) + +// VerifierType image verifier type - possible values are none and notation, when set to none image signatures wil not be verified. +type VerifierType string + +type containerVerifier interface { + Verify(context.Context, types.Image) error +} + +func newContainerVerifier(verifierType VerifierType, verifierConfig map[string]string, registryConfig map[string]*RegistryConfig) (containerVerifier, error) { + switch verifierType { + case VerifierNone: + return &skipVerifier{}, nil + case VerifierNotation: + return newNotationVerifier(verifierConfig, registryConfig) + default: + return nil, log.NewErrorf("unknown verifier type - %s", verifierType) + } +} + +type skipVerifier struct{} + +func (*skipVerifier) Verify(_ context.Context, _ types.Image) error { + return nil +} diff --git a/containerm/ctr/ctr_verifier_mock.go b/containerm/ctr/ctr_verifier_mock.go new file mode 100644 index 0000000..3f12a8a --- /dev/null +++ b/containerm/ctr/ctr_verifier_mock.go @@ -0,0 +1,62 @@ +// Copyright (c) 2023 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: ./containerm/ctr/ctr_verifier.go + +// Package mocks is a generated GoMock package. +package ctr + +import ( + context "context" + reflect "reflect" + + types "github.com/eclipse-kanto/container-management/containerm/containers/types" + gomock "github.com/golang/mock/gomock" +) + +// MockcontainerVerifier is a mock of containerVerifier interface. +type MockcontainerVerifier struct { + ctrl *gomock.Controller + recorder *MockcontainerVerifierMockRecorder +} + +// MockcontainerVerifierMockRecorder is the mock recorder for MockcontainerVerifier. +type MockcontainerVerifierMockRecorder struct { + mock *MockcontainerVerifier +} + +// NewMockcontainerVerifier creates a new mock instance. +func NewMockcontainerVerifier(ctrl *gomock.Controller) *MockcontainerVerifier { + mock := &MockcontainerVerifier{ctrl: ctrl} + mock.recorder = &MockcontainerVerifierMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockcontainerVerifier) EXPECT() *MockcontainerVerifierMockRecorder { + return m.recorder +} + +// Verify mocks base method. +func (m *MockcontainerVerifier) Verify(arg0 context.Context, arg1 types.Image) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Verify", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Verify indicates an expected call of Verify. +func (mr *MockcontainerVerifierMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockcontainerVerifier)(nil).Verify), arg0, arg1) +} diff --git a/containerm/ctr/ctr_verifier_notation.go b/containerm/ctr/ctr_verifier_notation.go new file mode 100644 index 0000000..164e1e9 --- /dev/null +++ b/containerm/ctr/ctr_verifier_notation.go @@ -0,0 +1,161 @@ +// Copyright (c) 2023 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +package ctr + +import ( + "context" + "fmt" + "net/http" + + "github.com/eclipse-kanto/container-management/containerm/containers/types" + "github.com/eclipse-kanto/container-management/containerm/log" + "github.com/notaryproject/notation-core-go/signature/cose" + "github.com/notaryproject/notation-core-go/signature/jws" + "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/registry" + "github.com/notaryproject/notation-go/verifier" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + orasregistry "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" +) + +// do not remove: support for verifying those types is added in init() functions in each corresponding packet, it seems that +// the packets does no go in the binary, through indirect dependencies, so add them explicitly otherwise verification fails. +var supportedMediaTypes = []string{jws.MediaTypeEnvelope, cose.MediaTypeEnvelope} + +type notationVerifier struct { + registryConfig map[string]*RegistryConfig +} + +func newNotationVerifier(config map[string]string, registryConfig map[string]*RegistryConfig) (containerVerifier, error) { + // set up notation configuration and library execution directories + if value, ok := config[notationKeyConfigDir]; ok { + dir.UserConfigDir = value + } + if value, ok := config[notationKeyLibexecDir]; ok { + dir.UserLibexecDir = value + } + return ¬ationVerifier{ + registryConfig: registryConfig, + }, nil +} + +func (nv *notationVerifier) Verify(ctx context.Context, imageInfo types.Image) error { + var ( + verifyOpts = notation.VerifyOptions{MaxSignatureAttempts: 50} + sigVerifier notation.Verifier + repo registry.Repository + err error + ) + + if sigVerifier, err = verifier.NewFromConfig(); err != nil { + return err + } + if repo, err = getRepository(imageInfo.Name, nv.registryConfig); err != nil { + return err + } + if _, verifyOpts.ArtifactReference, err = resolveReference(ctx, imageInfo.Name, repo); err != nil { + return err + } + + _, outcomes, err := notation.Verify(ctx, sigVerifier, repo, verifyOpts) + if err != nil { + return err + } else if len(outcomes) == 0 { + return log.NewErrorf("signature verification failed for all signatures of %s", imageInfo.Name) + } + + outcome := outcomes[0] + for _, result := range outcomes[0].VerificationResults { + if result.Error != nil { + if result.Action == trustpolicy.ActionLog { + log.WarnErr(result.Error, "%s verification failed", result.Type) + } + } + } + + if outcome.VerificationLevel.Name == trustpolicy.LevelSkip.Name { + log.Info("signature verification is skipped for %s", imageInfo.Name) + } else { + log.Info("signature verification is successful for %s", imageInfo.Name) + } + return nil +} + +func resolveReference(ctx context.Context, reference string, repo registry.Repository) (ocispec.Descriptor, string, error) { + var ( + ref orasregistry.Reference + manifestDesc ocispec.Descriptor + err error + ) + + if ref, err = orasregistry.ParseReference(reference); err != nil { + return ocispec.Descriptor{}, "", err + } + if manifestDesc, err = repo.Resolve(ctx, reference); err != nil { + return ocispec.Descriptor{}, "", err + } + + resolvedRef := fmt.Sprintf("%s/%s@%s", ref.Registry, ref.Repository, manifestDesc.Digest.String()) + if _, err := digest.Parse(ref.Reference); err != nil { + log.Warn("image %s is provided using a tag, tags are mutable, using a digest is the preferred way when verifying a signature", reference) + return manifestDesc, resolvedRef, nil + } + + if ref.Reference != manifestDesc.Digest.String() { + return ocispec.Descriptor{}, "", log.NewErrorf("provided digest %s does not match the resolved digest %s", ref.Reference, manifestDesc.Digest.String()) + } + return manifestDesc, resolvedRef, nil +} + +func getRepository(reference string, registryConfigs map[string]*RegistryConfig) (registry.Repository, error) { + var ( + err error + repo = &remote.Repository{} + ) + + if repo.Reference, err = orasregistry.ParseReference(reference); err != nil { + return nil, err + } + if repo.Client, repo.PlainHTTP, err = getAuthClient(repo.Reference, registryConfigs); err != nil { + return nil, err + } + return registry.NewRepository(repo), nil +} + +func getAuthClient(ref orasregistry.Reference, registryConfigs map[string]*RegistryConfig) (*auth.Client, bool, error) { + authClient := &auth.Client{ + Cache: auth.NewCache(), + Header: auth.DefaultClient.Header.Clone(), + } + + config, ok := registryConfigs[ref.Registry] + if !ok { + return authClient, false, nil + } + if config.Credentials != nil { + authClient.Credential = auth.StaticCredential(ref.Host(), auth.Credential{ + Username: config.Credentials.UserID, + Password: config.Credentials.Password, + }) + } + if !config.IsInsecure { + authClient.Client = &http.Client{Transport: getTransport(false, config.Transport, ref.Registry)} + } + + return authClient, config.IsInsecure, nil +} diff --git a/containerm/ctr/ctr_verifier_test.go b/containerm/ctr/ctr_verifier_test.go new file mode 100644 index 0000000..2ffb87d --- /dev/null +++ b/containerm/ctr/ctr_verifier_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +package ctr + +import ( + "context" + "testing" + + "github.com/eclipse-kanto/container-management/containerm/containers/types" + "github.com/eclipse-kanto/container-management/containerm/pkg/testutil" + "github.com/notaryproject/notation-go/dir" +) + +func TestContainerVerifier(t *testing.T) { + t.Run("unknown_verifier", func(t *testing.T) { + v, err := newContainerVerifier("unknown", nil, nil) + testutil.AssertNotNil(t, err) + testutil.AssertNil(t, v) + }) + t.Run("skip_verifier", func(t *testing.T) { + v, err := newContainerVerifier(VerifierNone, nil, nil) + testutil.AssertNil(t, err) + testutil.AssertNotNil(t, v) + testutil.AssertNil(t, v.Verify(context.Background(), types.Image{})) + }) + t.Run("notation_verifier", func(t *testing.T) { + config := map[string]string{ + notationKeyConfigDir: "testConfigDir", + notationKeyLibexecDir: "testLibexecDir", + } + registryConfig := map[string]*RegistryConfig{ + testHost: testRegConfig, + } + + v, err := newContainerVerifier(VerifierNotation, config, registryConfig) + testutil.AssertNil(t, err) + testutil.AssertNotNil(t, v) + testutil.AssertNotNil(t, v.Verify(context.Background(), types.Image{})) // expected fail due to invalid config dir + + nv := v.(*notationVerifier) + testutil.AssertEqual(t, registryConfig, nv.registryConfig) + testutil.AssertEqual(t, config[notationKeyConfigDir], dir.UserConfigDir) + testutil.AssertEqual(t, config[notationKeyLibexecDir], dir.UserLibexecDir) + + }) + +} diff --git a/containerm/ctr/ctrd_checkpoint.go b/containerm/ctr/ctrd_checkpoint.go index 3364ff5..97c11cc 100644 --- a/containerm/ctr/ctrd_checkpoint.go +++ b/containerm/ctr/ctrd_checkpoint.go @@ -83,7 +83,6 @@ func withCheckpointOpt(checkpoint *containerdtypes.Descriptor) containerd.NewTas // getCheckpointDir verifies checkpoint directory for create,remove, list options and checks if checkpoint already exists func getCheckpointDir(checkDir, checkpointID, ctrName, ctrID, ctrCheckpointDir string, create bool) (string, error) { var checkpointDir string - var err2 error if checkDir != "" { checkpointDir = checkDir } else { @@ -94,27 +93,27 @@ func getCheckpointDir(checkDir, checkpointID, ctrName, ctrID, ctrCheckpointDir s if create { switch { case err == nil && stat.IsDir(): - err2 = fmt.Errorf("checkpoint with name %s already exists for container %s", checkpointID, ctrName) + return checkpointAbsDir, fmt.Errorf("checkpoint with name %s already exists for container %s", checkpointID, ctrName) case err != nil && os.IsNotExist(err): - err2 = os.MkdirAll(checkpointAbsDir, 0700) + return checkpointAbsDir, os.MkdirAll(checkpointAbsDir, 0700) case err != nil: - err2 = err + return checkpointAbsDir, err case err == nil: - err2 = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) + return checkpointAbsDir, fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) default: // should never get here } } else { switch { case err != nil: - err2 = fmt.Errorf("checkpoint %s does not exist for container %s", checkpointID, ctrName) + return checkpointAbsDir, fmt.Errorf("checkpoint %s does not exist for container %s", checkpointID, ctrName) case err == nil && stat.IsDir(): - err2 = nil + return checkpointAbsDir, nil case err == nil: - err2 = fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) + return checkpointAbsDir, fmt.Errorf("%s exists and is not a directory", checkpointAbsDir) default: // should never get here } } - return checkpointAbsDir, err2 + return checkpointAbsDir, nil } diff --git a/containerm/ctr/ctrd_cio_mgr_internal.go b/containerm/ctr/ctrd_cio_mgr_internal.go index b2116f4..b36e668 100644 --- a/containerm/ctr/ctrd_cio_mgr_internal.go +++ b/containerm/ctr/ctrd_cio_mgr_internal.go @@ -19,7 +19,6 @@ package ctr import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" "sync" @@ -36,7 +35,7 @@ func (mgr *cioMgr) newFIFOSet(processID string, withStdin bool, withTerminal boo return nil, err } - fifoDir, err := ioutil.TempDir(mgr.fifoRootDir, "") + fifoDir, err := os.MkdirTemp(mgr.fifoRootDir, "") if err != nil { return nil, err } diff --git a/containerm/ctr/ctrd_client.go b/containerm/ctr/ctrd_client.go index b2cad6c..c396ecb 100644 --- a/containerm/ctr/ctrd_client.go +++ b/containerm/ctr/ctrd_client.go @@ -50,6 +50,7 @@ type containerdClient struct { ioMgr containerIOManager logsMgr containerLogsManager decMgr containerDecryptMgr + verifier containerVerifier spi containerdSpi eventsCancel context.CancelFunc runcRuntime types.Runtime diff --git a/containerm/ctr/ctrd_client_init.go b/containerm/ctr/ctrd_client_init.go index 4b4e9ce..e37ebd1 100644 --- a/containerm/ctr/ctrd_client_init.go +++ b/containerm/ctr/ctrd_client_init.go @@ -14,17 +14,19 @@ package ctr import ( "context" - "github.com/eclipse-kanto/container-management/containerm/containers/types" "path/filepath" "time" + "github.com/eclipse-kanto/container-management/containerm/containers/types" + "github.com/containerd/containerd" "github.com/eclipse-kanto/container-management/containerm/log" "github.com/eclipse-kanto/container-management/containerm/registry" "github.com/eclipse-kanto/container-management/containerm/util" ) -func newContainerdClient(namespace string, socket string, rootExec string, metaPath string, registryConfigs map[string]*RegistryConfig, imageDecKeys, imageDecRecipients []string, runcRuntime types.Runtime, imageExpiry time.Duration, imageExpiryDisable bool, leaseID string) (ContainerAPIClient, error) { +func newContainerdClient(namespace string, socket string, rootExec string, metaPath string, registryConfigs map[string]*RegistryConfig, imageDecKeys, imageDecRecipients []string, + runcRuntime types.Runtime, imageExpiry time.Duration, imageExpiryDisable bool, leaseID string, imageVerifierType VerifierType, imageVerifierConfig map[string]string) (ContainerAPIClient, error) { //ensure storage err := util.MkDir(rootExec) @@ -45,6 +47,10 @@ func newContainerdClient(namespace string, socket string, rootExec string, metaP if decrErr != nil { return nil, decrErr } + verifier, verifierErr := newContainerVerifier(imageVerifierType, imageVerifierConfig, registryConfigs) + if verifierErr != nil { + return nil, verifierErr + } ctrdClient := &containerdClient{ rootExec: rootExec, @@ -55,6 +61,7 @@ func newContainerdClient(namespace string, socket string, rootExec string, metaP ioMgr: newContainerIOManager(filepath.Join(rootExec, "fifo"), newCache()), logsMgr: newContainerLogsManager(filepath.Join(metaPath, "containers")), decMgr: decryptMgr, + verifier: verifier, runcRuntime: runcRuntime, imageExpiry: imageExpiry, imageExpiryDisable: imageExpiryDisable, @@ -78,5 +85,6 @@ func registryInit(registryCtx *registry.ServiceRegistryContext) (interface{}, er if err := applyOptsCtr(opts, createOpts...); err != nil { return nil, err } - return newContainerdClient(opts.namespace, opts.connectionPath, opts.rootExec, opts.metaPath, opts.registryConfigs, opts.imageDecKeys, opts.imageDecRecipients, opts.runcRuntime, opts.imageExpiry, opts.imageExpiryDisable, opts.leaseID) + return newContainerdClient(opts.namespace, opts.connectionPath, opts.rootExec, opts.metaPath, opts.registryConfigs, opts.imageDecKeys, opts.imageDecRecipients, + opts.runcRuntime, opts.imageExpiry, opts.imageExpiryDisable, opts.leaseID, opts.imageVerifierType, opts.imageVerifierConfig) } diff --git a/containerm/ctr/ctrd_client_internal.go b/containerm/ctr/ctrd_client_internal.go index 4677314..a8711ff 100644 --- a/containerm/ctr/ctrd_client_internal.go +++ b/containerm/ctr/ctrd_client_internal.go @@ -18,10 +18,6 @@ import ( "syscall" "time" - "github.com/opencontainers/image-spec/identity" - - "golang.org/x/sys/unix" - "github.com/containerd/containerd" "github.com/containerd/containerd/api/events" "github.com/containerd/containerd/errdefs" @@ -32,6 +28,8 @@ import ( "github.com/eclipse-kanto/container-management/containerm/containers/types" "github.com/eclipse-kanto/container-management/containerm/log" "github.com/eclipse-kanto/container-management/containerm/util" + "github.com/opencontainers/image-spec/identity" + "golang.org/x/sys/unix" ) func (ctrdClient *containerdClient) generateRemoteOpts(imageInfo types.Image) []containerd.RemoteOpt { @@ -116,6 +114,9 @@ func (ctrdClient *containerdClient) pullImage(ctx context.Context, imageInfo typ if err != nil { // if the image is not present locally - pull it if errdefs.IsNotFound(err) { + if err = ctrdClient.verifier.Verify(ctx, imageInfo); err != nil { + return nil, err + } ctrdImage, err = ctrdClient.spi.PullImage(ctx, imageInfo.Name, ctrdClient.generateRemoteOpts(imageInfo)...) if err != nil { return nil, err diff --git a/containerm/ctr/ctrd_client_internal_test.go b/containerm/ctr/ctrd_client_internal_test.go index dddf6ef..86f6a4a 100644 --- a/containerm/ctr/ctrd_client_internal_test.go +++ b/containerm/ctr/ctrd_client_internal_test.go @@ -113,6 +113,56 @@ func TestClientInternalGenerateNewContainerOpts(t *testing.T) { } } +func TestConfigureRuncRuntime(t *testing.T) { + ctrdClient := &containerdClient{ + runcRuntime: types.RuntimeTypeV2runcV2, + } + tests := map[string]struct { + container *types.Container + runtime types.Runtime + }{ + "test_RuntimeTypeV1": { + container: &types.Container{ + HostConfig: &types.HostConfig{ + Runtime: types.RuntimeTypeV1, + }, + }, + runtime: ctrdClient.runcRuntime, + }, + "RuntimeTypeV2runcV1": { + container: &types.Container{ + HostConfig: &types.HostConfig{ + Runtime: types.RuntimeTypeV2runcV1, + }, + }, + runtime: ctrdClient.runcRuntime, + }, + "RuntimeTypeV2runcV2": { + container: &types.Container{ + HostConfig: &types.HostConfig{ + Runtime: types.RuntimeTypeV2runcV2, + }, + }, + runtime: ctrdClient.runcRuntime, + }, + "test_default": { + container: &types.Container{ + HostConfig: &types.HostConfig{ + Runtime: types.RuntimeTypeV2kataV2, + }, + }, + runtime: types.RuntimeTypeV2kataV2, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctrdClient.configureRuncRuntime(test.container) + testutil.AssertEqual(t, test.runtime, test.container.HostConfig.Runtime) + }) + } +} + func TestClientInternalGenerateRemoteOpts(t *testing.T) { const containerImageRef = "some.repo/image:tag" testImageInfo := types.Image{ @@ -275,17 +325,17 @@ func TestClientInternalPullImage(t *testing.T) { DecryptConfig: &types.DecryptConfig{}, } testCases := map[string]struct { - mockExec func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) + mockExec func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) }{ "test_get_decrypt_cfg_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { err := log.NewError("test error") decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(nil, err) return nil, err }, }, "test_spi_get_image_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil) err := log.NewError("test error") @@ -294,7 +344,7 @@ func TestClientInternalPullImage(t *testing.T) { }, }, "test_spi_get_image_available_check_auth_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil) imageMock := mocksContainerd.NewMockImage(ctrl) @@ -305,7 +355,7 @@ func TestClientInternalPullImage(t *testing.T) { }, }, "test_spi_get_image_available_no_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil) imageMock := mocksContainerd.NewMockImage(ctrl) @@ -314,11 +364,22 @@ func TestClientInternalPullImage(t *testing.T) { return imageMock, nil }, }, + "test_spi_get_image_not_available_verifier_error": { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { + dc := &config.DecryptConfig{} + decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil) + spiMock.EXPECT().GetImage(gomock.Any(), testImageInfo.Name).Return(nil, errdefs.ErrNotFound) + err := log.NewError("test error") + verifierMock.EXPECT().Verify(gomock.Any(), testImageInfo).Return(err) + return nil, err + }, + }, "test_spi_get_image_not_available_pull_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil) spiMock.EXPECT().GetImage(gomock.Any(), testImageInfo.Name).Return(nil, errdefs.ErrNotFound) + verifierMock.EXPECT().Verify(gomock.Any(), testImageInfo).Return(nil) regsResolverMock.EXPECT().ResolveImageRegistry(util.GetImageHost(testImageInfo.Name)).Return(nil) err := log.NewError("test error") spiMock.EXPECT().PullImage(gomock.Any(), testImageInfo.Name, matchers.MatchesResolverOpts(containerd.WithSchema1Conversion)).Return(nil, err) @@ -326,10 +387,11 @@ func TestClientInternalPullImage(t *testing.T) { }, }, "test_spi_get_image_not_available_check_auth_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil) spiMock.EXPECT().GetImage(gomock.Any(), testImageInfo.Name).Return(nil, errdefs.ErrNotFound) + verifierMock.EXPECT().Verify(gomock.Any(), testImageInfo).Return(nil) regsResolverMock.EXPECT().ResolveImageRegistry(util.GetImageHost(testImageInfo.Name)).Return(nil) imageMock := mocksContainerd.NewMockImage(ctrl) spiMock.EXPECT().PullImage(gomock.Any(), testImageInfo.Name, matchers.MatchesResolverOpts(containerd.WithSchema1Conversion)).Return(imageMock, nil) @@ -339,10 +401,11 @@ func TestClientInternalPullImage(t *testing.T) { }, }, "test_spi_get_image_not_available_gen_unpack_opts_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil) spiMock.EXPECT().GetImage(gomock.Any(), testImageInfo.Name).Return(nil, errdefs.ErrNotFound) + verifierMock.EXPECT().Verify(gomock.Any(), testImageInfo).Return(nil) regsResolverMock.EXPECT().ResolveImageRegistry(util.GetImageHost(testImageInfo.Name)).Return(nil) imageMock := mocksContainerd.NewMockImage(ctrl) spiMock.EXPECT().PullImage(gomock.Any(), testImageInfo.Name, matchers.MatchesResolverOpts(containerd.WithSchema1Conversion)).Return(imageMock, nil) @@ -353,10 +416,11 @@ func TestClientInternalPullImage(t *testing.T) { }, }, "test_spi_get_image_not_available_unpack_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil).Times(2) spiMock.EXPECT().GetImage(gomock.Any(), testImageInfo.Name).Return(nil, errdefs.ErrNotFound) + verifierMock.EXPECT().Verify(gomock.Any(), testImageInfo).Return(nil) regsResolverMock.EXPECT().ResolveImageRegistry(util.GetImageHost(testImageInfo.Name)).Return(nil) imageMock := mocksContainerd.NewMockImage(ctrl) spiMock.EXPECT().PullImage(gomock.Any(), testImageInfo.Name, matchers.MatchesResolverOpts(containerd.WithSchema1Conversion)).Return(imageMock, nil) @@ -367,10 +431,11 @@ func TestClientInternalPullImage(t *testing.T) { }, }, "test_spi_get_image_not_available_no_error": { - mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, ctrl *gomock.Controller) (containerd.Image, error) { + mockExec: func(decryptMgrMock *mocksCtrd.MockcontainerDecryptMgr, spiMock *mocksCtrd.MockcontainerdSpi, regsResolverMock *mocksCtrd.MockcontainerImageRegistriesResolver, verifierMock *MockcontainerVerifier, ctrl *gomock.Controller) (containerd.Image, error) { dc := &config.DecryptConfig{} decryptMgrMock.EXPECT().GetDecryptConfig(testImageInfo.DecryptConfig).Return(dc, nil).Times(2) spiMock.EXPECT().GetImage(gomock.Any(), testImageInfo.Name).Return(nil, errdefs.ErrNotFound) + verifierMock.EXPECT().Verify(gomock.Any(), testImageInfo).Return(nil) regsResolverMock.EXPECT().ResolveImageRegistry(util.GetImageHost(testImageInfo.Name)).Return(nil) imageMock := mocksContainerd.NewMockImage(ctrl) spiMock.EXPECT().PullImage(gomock.Any(), testImageInfo.Name, matchers.MatchesResolverOpts(containerd.WithSchema1Conversion)).Return(imageMock, nil) @@ -389,12 +454,14 @@ func TestClientInternalPullImage(t *testing.T) { decryptMgrMock := mocksCtrd.NewMockcontainerDecryptMgr(ctrl) spiMock := mocksCtrd.NewMockcontainerdSpi(ctrl) registriesResolverMock := mocksCtrd.NewMockcontainerImageRegistriesResolver(ctrl) + verifierMock := NewMockcontainerVerifier(ctrl) ctrdClient := &containerdClient{ decMgr: decryptMgrMock, spi: spiMock, registriesResolver: registriesResolverMock, + verifier: verifierMock, } - expectedImage, expectedErr := testCaseData.mockExec(decryptMgrMock, spiMock, registriesResolverMock, ctrl) + expectedImage, expectedErr := testCaseData.mockExec(decryptMgrMock, spiMock, registriesResolverMock, verifierMock, ctrl) actualImage, actualErr := ctrdClient.pullImage(context.TODO(), testImageInfo) testutil.AssertError(t, expectedErr, actualErr) testutil.AssertEqual(t, expectedImage, actualImage) diff --git a/containerm/ctr/ctrd_client_test.go b/containerm/ctr/ctrd_client_test.go index 9c58c3e..307218e 100644 --- a/containerm/ctr/ctrd_client_test.go +++ b/containerm/ctr/ctrd_client_test.go @@ -14,6 +14,7 @@ package ctr import ( "context" + "io" "reflect" "testing" "time" @@ -24,7 +25,9 @@ import ( "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/matchers" containerdMocks "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/containerd" ctrdMocks "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/ctrd" + ioMocks "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/io" loggerMocks "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/logger" + streamsMocks "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/streams" "github.com/eclipse-kanto/container-management/containerm/streams" "github.com/eclipse-kanto/container-management/containerm/util" @@ -140,9 +143,7 @@ func TestCtrdClientCreateContainer(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - expectedError := testCase.mockExec() - actualError := testClient.CreateContainer(ctx, testCtr, "") - testutil.AssertError(t, expectedError, actualError) + testutil.AssertError(t, testCase.mockExec(), testClient.CreateContainer(ctx, testCtr, "")) }) } } @@ -555,12 +556,16 @@ func TestAttachContainer(t *testing.T) { defer mockCtrl.Finish() mockIoMgr := NewMockcontainerIOManager(mockCtrl) + mockReadCloser := ioMocks.NewMockReadCloser(mockCtrl) + mockStream := streamsMocks.NewMockStream(mockCtrl) + mockIO := NewMockIO(mockCtrl) + attachConfig := &streams.AttachConfig{UseStdin: true, Stdin: mockReadCloser} + ctx := context.Background() testClient := &containerdClient{ ioMgr: mockIoMgr, } - ctx := context.Background() testCtr := &types.Container{ ID: "test-id", IOConfig: &types.IOConfig{ @@ -569,13 +574,60 @@ func TestAttachContainer(t *testing.T) { }, } - expectedError := log.NewErrorf("failed to initialise IO for container ID = test-id") + tests := map[string]struct { + attachConfig *streams.AttachConfig + mockExec func() error + }{ + "test_containerIO_nil": { + attachConfig: attachConfig, + mockExec: func() error { + mockIoMgr.EXPECT().GetIO(testCtr.ID).Return(nil) + mockIoMgr.EXPECT().InitIO(testCtr.ID, testCtr.IOConfig.OpenStdin).Return(nil, log.NewError("failed to initialise IO for container ID = test-id")) + return log.NewError("failed to initialise IO for container ID = test-id") + }, + }, + "test_container_stdin_true": { + attachConfig: attachConfig, + mockExec: func() error { + errChan := make(chan error) + go func() { + errChan <- nil + close(errChan) + }() + + mockIoMgr.EXPECT().GetIO(testCtr.ID).Return(mockIO) + mockIO.EXPECT().Stream().Return(mockStream) + mockReadCloser.EXPECT().Read(gomock.Any()).DoAndReturn(func(p []byte) (int, error) { + return -1, io.EOF + }).AnyTimes() + mockStream.EXPECT().Attach(ctx, gomock.AssignableToTypeOf(attachConfig)).Return(errChan) + + return nil + }, + }, + "test_container_stdin_false": { + attachConfig: &streams.AttachConfig{Stdin: mockReadCloser}, + mockExec: func() error { + errChan := make(chan error) + go func() { + errChan <- nil + close(errChan) + }() - mockIoMgr.EXPECT().GetIO(testCtr.ID).Return(nil) - mockIoMgr.EXPECT().InitIO(testCtr.ID, testCtr.IOConfig.OpenStdin).Return(nil, expectedError) + mockIoMgr.EXPECT().GetIO(testCtr.ID).Return(mockIO) + mockIO.EXPECT().Stream().Return(mockStream) + mockStream.EXPECT().Attach(ctx, gomock.AssignableToTypeOf(attachConfig)).Return(errChan) - actualError := testClient.AttachContainer(ctx, testCtr, &streams.AttachConfig{}) - testutil.AssertError(t, expectedError, actualError) + return nil + }, + }, + } + + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + testutil.AssertError(t, testCase.mockExec(), testClient.AttachContainer(ctx, testCtr, testCase.attachConfig)) + }) + } } func TestPauseContainer(t *testing.T) { @@ -633,9 +685,7 @@ func TestPauseContainer(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - expectedError := testCase.mockExec(ctx, mockTask) - actualError := testClient.PauseContainer(ctx, testCase.arg) - testutil.AssertError(t, expectedError, actualError) + testutil.AssertError(t, testCase.mockExec(ctx, mockTask), testClient.PauseContainer(ctx, testCase.arg)) }) } } @@ -695,9 +745,7 @@ func TestUnpauseContainer(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - expectedError := testCase.mockExec(ctx, mockTask) - actualError := testClient.UnpauseContainer(ctx, testCase.arg) - testutil.AssertError(t, expectedError, actualError) + testutil.AssertError(t, testCase.mockExec(ctx, mockTask), testClient.UnpauseContainer(ctx, testCase.arg)) }) } } @@ -886,9 +934,7 @@ func TestRestoreContainer(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - expectedError := testCase.mockExec() - actualError := testClient.RestoreContainer(ctx, testCtr) - testutil.AssertError(t, expectedError, actualError) + testutil.AssertError(t, testCase.mockExec(), testClient.RestoreContainer(ctx, testCtr)) }) } } @@ -923,11 +969,10 @@ func TestSetContainerExitHooks(t *testing.T) { testClient.SetContainerExitHooks(arg) - got := testClient.ctrdCache.containerExitHooks - testutil.AssertEqual(t, 1, len(got)) + testutil.AssertEqual(t, 1, len(testClient.ctrdCache.containerExitHooks)) - if reflect.ValueOf(got[0]).Pointer() != reflect.ValueOf(arg).Pointer() { - t.Errorf("SetContainerExitHooks() = %v, want %v", reflect.ValueOf(got[0]), reflect.ValueOf(arg)) + if reflect.ValueOf(testClient.ctrdCache.containerExitHooks[0]).Pointer() != reflect.ValueOf(arg).Pointer() { + t.Errorf("SetContainerExitHooks() = %v, want %v", reflect.ValueOf(testClient.ctrdCache.containerExitHooks[0]), reflect.ValueOf(arg)) } } @@ -1118,9 +1163,7 @@ func TestCtrdClientUpdateContainer(t *testing.T) { task: mockTask, } - expectedErr := testCase.mockExec() - actualErr := testClient.UpdateContainer(ctx, testCase.ctr, testCase.resources) - testutil.AssertError(t, expectedErr, actualErr) + testutil.AssertError(t, testCase.mockExec(), testClient.UpdateContainer(ctx, testCase.ctr, testCase.resources)) }) } } diff --git a/containerm/ctr/ctrd_container_create_opts_test.go b/containerm/ctr/ctrd_container_create_opts_test.go new file mode 100644 index 0000000..3711879 --- /dev/null +++ b/containerm/ctr/ctrd_container_create_opts_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +package ctr + +import ( + "testing" + + "github.com/eclipse-kanto/container-management/containerm/containers/types" + "github.com/eclipse-kanto/container-management/containerm/pkg/testutil" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/images" +) + +func TestWithRuntimeOpts(t *testing.T) { + tests := map[string]struct { + container *types.Container + }{ + "test_runtime_type_v1": { + &types.Container{ + ID: testCtrID1, + Name: testContainerName, + HostConfig: &types.HostConfig{ + Runtime: types.RuntimeTypeV1, + }, + }, + }, + "test_runtime_type_v2": { + &types.Container{ + ID: testCtrID1, + Name: testContainerName, + HostConfig: &types.HostConfig{ + Runtime: types.RuntimeTypeV2runcV1, + }, + }, + }, + "testing_default": { + &types.Container{ + ID: testCtrID1, + Name: testContainerName, + HostConfig: &types.HostConfig{ + Runtime: "", + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + testutil.AssertNotNil(t, WithRuntimeOpts(test.container, "")) + }) + } +} + +func TestWithSpecOpts(t *testing.T) { + tests := map[string]struct { + container *types.Container + }{ + "test_config": { + &types.Container{ + Config: &types.ContainerConfiguration{ + Cmd: []string{"test"}, + Env: []string{"test"}, + }, + HostConfig: &types.HostConfig{}, + }, + }, + "test_privileged": { + &types.Container{ + HostConfig: &types.HostConfig{ + Privileged: true, + }, + }, + }, + "test_extra_capabilities": { + &types.Container{ + HostConfig: &types.HostConfig{ + ExtraCapabilities: []string{"CAP_NET_ADMIN"}, + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + testutil.AssertNotNil(t, WithSpecOpts(test.container, containerd.NewImage(&containerd.Client{}, images.Image{}), "/tmp/test")) + }) + } +} diff --git a/containerm/ctr/ctrd_oci_linux.go b/containerm/ctr/ctrd_oci_linux.go index d69d8dd..b7a919d 100644 --- a/containerm/ctr/ctrd_oci_linux.go +++ b/containerm/ctr/ctrd_oci_linux.go @@ -14,6 +14,7 @@ package ctr import ( "context" + "fmt" "os" "path/filepath" "strconv" @@ -137,6 +138,14 @@ func WithHooks(container *types.Container, execRoot string) crtdoci.SpecOpts { if err != nil { return err } + + var env []string // set XDG_CONFIG_HOME and HOME as they are needed for notation init, otherwise libnet hook execution fails + for _, key := range []string{"XDG_CONFIG_HOME", "HOME"} { + value := os.Getenv(key) + if value != "" { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + } libnetHook := specs.Hook{ Path: target, Args: []string{ @@ -145,6 +154,7 @@ func WithHooks(container *types.Container, execRoot string) crtdoci.SpecOpts { container.ID, stringid.TruncateID(container.NetworkSettings.NetworkControllerID), }, + Env: env, } s.Hooks.Prestart = append(s.Hooks.Prestart, libnetHook) } diff --git a/containerm/ctr/ctrd_registries_resolver.go b/containerm/ctr/ctrd_registries_resolver.go index b9ff398..d2bbd46 100644 --- a/containerm/ctr/ctrd_registries_resolver.go +++ b/containerm/ctr/ctrd_registries_resolver.go @@ -13,7 +13,6 @@ package ctr import ( - "net" "net/http" "time" @@ -100,7 +99,6 @@ func (resolver *ctrImagesResolver) processImageRegistries() { //needed for insecure registries with self-signed certificates var httpConfig docker.RegistryHost - tlsConfig := createDefaultTLSConfig(config.IsInsecure) if config.Transport != nil && config.IsInsecure { log.Warn("a TLS configuration for registry host %s is provided but the registry is marked as insecure - the TLS config will not be applied", host) } @@ -117,32 +115,9 @@ func (resolver *ctrImagesResolver) processImageRegistries() { if config.Credentials != nil { httpConfig.Authorizer = docker.NewDockerAuthorizer(docker.WithAuthClient(http.DefaultClient), docker.WithAuthCreds(resolver.getRegistryAuthCreds)) } - } else { - if config.Transport != nil { - if err := applyLocalTLSConfig(config.Transport, tlsConfig); err != nil { - log.WarnErr(err, "could not process provided TLS configuration - default will be used for registry host %s", host) - tlsConfig = createDefaultTLSConfig(config.IsInsecure) - } else { - log.Debug("successfully applied TLS configuration for registry host %s", host) - } - } - } - - tr := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: registryResolverDialContextTimeout, - KeepAlive: registryResolverDialContextKeepAlive, - DualStack: true, - }).DialContext, - MaxIdleConns: registryResolverTransportMaxIdeConns, - IdleConnTimeout: registryResolverTransportIdleConnTimeout, - TLSHandshakeTimeout: registryResolverTransportTLSHandshakeTimeout, - TLSClientConfig: tlsConfig, - ExpectContinueTimeout: registryResolverTransportExpectContinueTimeout, } - httpsClient := &http.Client{Transport: tr} + httpsClient := &http.Client{Transport: getTransport(config.IsInsecure, config.Transport, host)} httpsConfig := docker.RegistryHost{ Client: httpsClient, Host: host, diff --git a/containerm/daemon/daemon_command.go b/containerm/daemon/daemon_command.go index 2677129..f21ae67 100644 --- a/containerm/daemon/daemon_command.go +++ b/containerm/daemon/daemon_command.go @@ -42,7 +42,7 @@ func setupCommandFlags(cmd *cobra.Command) { flagSet.StringVar(&cfg.ManagerConfig.MgrExecPath, "cm-exec-root-dir", cfg.ManagerConfig.MgrExecPath, "Specify the exec root directory of the container manager service") flagSet.StringVar(&cfg.ManagerConfig.MgrCtrClientServiceID, "cm-cc-sid", cfg.ManagerConfig.MgrCtrClientServiceID, "Specify the ID of the container runtime client service to be used by the container manager service") flagSet.StringVar(&cfg.ManagerConfig.MgrNetMgrServiceID, "cm-net-sid", cfg.ManagerConfig.MgrNetMgrServiceID, "Specify the ID of the network manager service to be used by container manager service") - flagSet.Int64Var(&cfg.ManagerConfig.MgrDefaultCtrsStopTimeout, "cm-deflt-ctrs-stop-timeout", cfg.ManagerConfig.MgrDefaultCtrsStopTimeout, "Specify the default timeout that the container manager service will wait before killing the container's process") + flagSet.StringVar(&cfg.ManagerConfig.MgrDefaultCtrsStopTimeout, "cm-deflt-ctrs-stop-timeout", cfg.ManagerConfig.MgrDefaultCtrsStopTimeout, "Specify the default timeout that the container manager service will wait before killing the container's process") // init container client flags flagSet.StringVar(&cfg.ContainerClientConfig.CtrNamespace, "ccl-default-ns", cfg.ContainerClientConfig.CtrNamespace, "Specify the default namespace to be used for container management isolation") @@ -56,6 +56,8 @@ func setupCommandFlags(cmd *cobra.Command) { flagSet.DurationVar(&cfg.ContainerClientConfig.CtrImageExpiry, "ccl-image-expiry", cfg.ContainerClientConfig.CtrImageExpiry, "Specify the time period for the cached images and content to be kept in the form of e.g. 72h3m0.5s") flagSet.BoolVar(&cfg.ContainerClientConfig.CtrImageExpiryDisable, "ccl-image-expiry-disable", cfg.ContainerClientConfig.CtrImageExpiryDisable, "Disables expiry management of cached images and content - must be used with caution as it may lead to large memory volumes being persistently allocated") flagSet.StringVar(&cfg.ContainerClientConfig.CtrLeaseID, "ccl-lease-id", cfg.ContainerClientConfig.CtrLeaseID, "Specify the lease identifier to be used for container resources persistence") + flagSet.StringVar(&cfg.ContainerClientConfig.CtrImageVerifierType, "ccl-image-verifier-type", cfg.ContainerClientConfig.CtrImageVerifierType, "Specify the image verifier type - possible values are none and notation, when set to none image signatures wil not be verified.") + flagSet.Var(&cfg.ContainerClientConfig.CtrImageVerifierConfig, "ccl-image-verifier-config", "Specify the configuration of the image verifier, as comma separated {key}={value} pairs - possible keys for notation verifier are configDir and libexecDir, for more info https://notaryproject.dev/docs/user-guides/how-to/directory-structure/#user-level") // init network manager flags flagSet.StringVar(&cfg.NetworkConfig.NetType, "net-type", cfg.NetworkConfig.NetType, "Specify the default network management type for containers") diff --git a/containerm/daemon/daemon_config.go b/containerm/daemon/daemon_config.go index d071b59..5b46359 100644 --- a/containerm/daemon/daemon_config.go +++ b/containerm/daemon/daemon_config.go @@ -14,6 +14,9 @@ package main import ( "encoding/json" + "fmt" + "strconv" + "strings" "time" "github.com/eclipse-kanto/container-management/containerm/log" @@ -46,23 +49,51 @@ type managerConfig struct { MgrExecPath string `json:"exec_root_dir,omitempty"` MgrCtrClientServiceID string `json:"container_client_sid,omitempty"` MgrNetMgrServiceID string `json:"network_manager_sid,omitempty"` - MgrDefaultCtrsStopTimeout int64 `json:"default_ctrs_stop_timeout,omitempty"` + MgrDefaultCtrsStopTimeout string `json:"default_ctrs_stop_timeout,omitempty"` +} + +func (mc *managerConfig) UnmarshalJSON(data []byte) error { + type managerConfigPlain managerConfig + + plain := (*managerConfigPlain)(mc) + err := json.Unmarshal(data, &plain) + if err == nil { + return nil + } + + tmp := struct { + MgrDefaultCtrsStopTimeout int `json:"default_ctrs_stop_timeout,omitempty"` + *managerConfigPlain + }{ + managerConfigPlain: plain, + } + + if err = json.Unmarshal(data, &tmp); err != nil { + return err + } + + if tmp.MgrDefaultCtrsStopTimeout != 0 { + mc.MgrDefaultCtrsStopTimeout = strconv.Itoa(tmp.MgrDefaultCtrsStopTimeout) + } + return nil } // container client config- e.g. containerd type containerRuntimeConfig struct { - CtrNamespace string `json:"default_ns,omitempty"` - CtrAddressPath string `json:"address_path,omitempty"` - CtrRegistryConfigs map[string]*registryConfig `json:"registry_configurations,omitempty"` - CtrInsecureRegistries []string `json:"insecure_registries,omitempty"` - CtrRootExec string `json:"exec_root_dir,omitempty"` - CtrMetaPath string `json:"home_dir,omitempty"` - CtrImageDecKeys []string `json:"image_dec_keys,omitempty"` - CtrImageDecRecipients []string `json:"image_dec_recipients,omitempty"` - CtrRuncRuntime string `json:"runc_runtime,omitempty"` - CtrImageExpiry time.Duration `json:"image_expiry,omitempty"` - CtrImageExpiryDisable bool `json:"image_expiry_disable,omitempty"` - CtrLeaseID string `json:"lease_id,omitempty"` + CtrNamespace string `json:"default_ns,omitempty"` + CtrAddressPath string `json:"address_path,omitempty"` + CtrRegistryConfigs map[string]*registryConfig `json:"registry_configurations,omitempty"` + CtrInsecureRegistries []string `json:"insecure_registries,omitempty"` + CtrRootExec string `json:"exec_root_dir,omitempty"` + CtrMetaPath string `json:"home_dir,omitempty"` + CtrImageDecKeys []string `json:"image_dec_keys,omitempty"` + CtrImageDecRecipients []string `json:"image_dec_recipients,omitempty"` + CtrRuncRuntime string `json:"runc_runtime,omitempty"` + CtrImageExpiry time.Duration `json:"image_expiry,omitempty"` + CtrImageExpiryDisable bool `json:"image_expiry_disable,omitempty"` + CtrLeaseID string `json:"lease_id,omitempty"` + CtrImageVerifierType string `json:"image_verifier_type,omitempty"` + CtrImageVerifierConfig verifierConfig `json:"image_verifier_config,omitempty"` } // deployment manager config @@ -189,3 +220,44 @@ type thingsConnectionConfig struct { UnsubscribeTimeout int64 `json:"unsubscribe_timeout,omitempty"` Transport *tlsConfig `json:"transport,omitempty"` } + +// verifierConfig is alias used as flag value +type verifierConfig map[string]string + +// String is representation of verifierConfig as a comma separated {key}={value} pairs +func (vc *verifierConfig) String() string { + if len(*vc) == 0 { + return "" + } + var pairs []string + for key, value := range *vc { + pairs = append(pairs, fmt.Sprintf("%s=%s", key, value)) + } + + return strings.Join(pairs, ",") +} + +// Set verifierConfig from string, used for flag set +func (vc *verifierConfig) Set(value string) error { + if value == "" { + return log.NewError("the image verifier config could not be empty") + } + if *vc == nil { + *vc = make(map[string]string) + } + + pairs := strings.Split(value, ",") + for _, pair := range pairs { + key, value, ok := strings.Cut(pair, "=") + if !ok || key == "" || value == "" { + return log.NewErrorf("could not parse image verification config, invalid key-value pair - %s", pair) + } + (*vc)[key] = value + } + return nil +} + +// Type returns the verifierConfig flag type +func (vc verifierConfig) Type() string { + return "stringSlice" +} diff --git a/containerm/daemon/daemon_config_default.go b/containerm/daemon/daemon_config_default.go index c279fbd..d121c85 100644 --- a/containerm/daemon/daemon_config_default.go +++ b/containerm/daemon/daemon_config_default.go @@ -40,7 +40,7 @@ const ( managerExecRootPathDefault = "/var/run/container-management" managerContainerClientServiceIDDefault = ctr.ContainerdClientServiceLocalID managerNetworkManagerServiceIDDefault = network.LibnetworkManagerServiceLocalID - managerNetworkManagerStopTimeout = 30 + managerContainerStopTimeoutDefault = "30s" // default container client config containerClientNamespaceDefault = "kanto-cm" @@ -51,6 +51,7 @@ const ( containerClientImageExpiry = 31 * 24 * time.Hour // 31 days containerClientImageExpiryDisable = false containerClientLeaseIDDefault = "kanto-cm.lease" + containerClientImageVerifierType = string(ctr.VerifierNone) // default network manager config networkManagerNetTypeDefault = string(types.NetworkModeBridge) @@ -125,7 +126,7 @@ func getDefaultInstance() *config { MgrExecPath: managerExecRootPathDefault, MgrCtrClientServiceID: managerContainerClientServiceIDDefault, MgrNetMgrServiceID: managerNetworkManagerServiceIDDefault, - MgrDefaultCtrsStopTimeout: managerNetworkManagerStopTimeout, + MgrDefaultCtrsStopTimeout: managerContainerStopTimeoutDefault, }, ContainerClientConfig: &containerRuntimeConfig{ CtrNamespace: containerClientNamespaceDefault, @@ -137,6 +138,7 @@ func getDefaultInstance() *config { CtrImageExpiry: containerClientImageExpiry, CtrImageExpiryDisable: containerClientImageExpiryDisable, CtrLeaseID: containerClientLeaseIDDefault, + CtrImageVerifierType: containerClientImageVerifierType, }, NetworkConfig: &networkConfig{ NetType: networkManagerNetTypeDefault, diff --git a/containerm/daemon/daemon_config_util.go b/containerm/daemon/daemon_config_util.go index 00099ee..b36fd5b 100644 --- a/containerm/daemon/daemon_config_util.go +++ b/containerm/daemon/daemon_config_util.go @@ -18,6 +18,7 @@ import ( "io/ioutil" "os" "reflect" + "strconv" "time" "github.com/eclipse-kanto/container-management/containerm/containers/types" @@ -48,6 +49,8 @@ func extractCtrClientConfigOptions(daemonConfig *config) []ctr.ContainerOpts { ctr.WithCtrdImageExpiry(daemonConfig.ContainerClientConfig.CtrImageExpiry), ctr.WithCtrdImageExpiryDisable(daemonConfig.ContainerClientConfig.CtrImageExpiryDisable), ctr.WithCtrdLeaseID(daemonConfig.ContainerClientConfig.CtrLeaseID), + ctr.WithCtrImageVerifierType(daemonConfig.ContainerClientConfig.CtrImageVerifierType), + ctr.WithCtrImageVerifierConfig(daemonConfig.ContainerClientConfig.CtrImageVerifierConfig), ) return ctrOpts } @@ -76,12 +79,15 @@ func extractNetManagerConfigOptions(daemonConfig *config) []network.NetOpt { func extractContainerManagerOptions(daemonConfig *config) []mgr.ContainerManagerOpt { mgrOpts := []mgr.ContainerManagerOpt{} + if _, err := strconv.Atoi(daemonConfig.ManagerConfig.MgrDefaultCtrsStopTimeout); err == nil { + daemonConfig.ManagerConfig.MgrDefaultCtrsStopTimeout = fmt.Sprintf("%ss", daemonConfig.ManagerConfig.MgrDefaultCtrsStopTimeout) + } mgrOpts = append(mgrOpts, mgr.WithMgrMetaPath(daemonConfig.ManagerConfig.MgrMetaPath), mgr.WithMgrRootExec(daemonConfig.ManagerConfig.MgrExecPath), mgr.WithMgrContainerClientServiceID(daemonConfig.ManagerConfig.MgrCtrClientServiceID), mgr.WithMgrNetworkManagerServiceID(daemonConfig.ManagerConfig.MgrNetMgrServiceID), - mgr.WithMgrDefaultContainerStopTimeout(daemonConfig.ManagerConfig.MgrDefaultCtrsStopTimeout), + mgr.WithMgrDefaultContainerStopTimeout(parseDuration(daemonConfig.ManagerConfig.MgrDefaultCtrsStopTimeout, managerContainerStopTimeoutDefault)), ) return mgrOpts } @@ -263,7 +269,7 @@ func dumpContManager(configInstance *config) { log.Debug("[daemon_cfg][cm-exec-root-dir] : %s", configInstance.ManagerConfig.MgrExecPath) log.Debug("[daemon_cfg][cm-cc-sid] : %s", configInstance.ManagerConfig.MgrCtrClientServiceID) log.Debug("[daemon_cfg][cm-net-sid] : %s", configInstance.ManagerConfig.MgrNetMgrServiceID) - log.Debug("[daemon_cfg][cm-deflt-ctrs-stop-timeout] : %d", configInstance.ManagerConfig.MgrDefaultCtrsStopTimeout) + log.Debug("[daemon_cfg][cm-deflt-ctrs-stop-timeout] : %s", configInstance.ManagerConfig.MgrDefaultCtrsStopTimeout) } } @@ -288,6 +294,8 @@ func dumpContClient(configInstance *config) { log.Debug("[daemon_cfg][ccl-image-expiry] : %s", configInstance.ContainerClientConfig.CtrImageExpiry) log.Debug("[daemon_cfg][ccl-image-expiry-disable] : %v", configInstance.ContainerClientConfig.CtrImageExpiryDisable) log.Debug("[daemon_cfg][ccl-lease-id] : %s", configInstance.ContainerClientConfig.CtrLeaseID) + log.Debug("[daemon_cfg][ccl-image-verifier-type] : %v", configInstance.ContainerClientConfig.CtrImageVerifierType) + log.Debug("[daemon_cfg][ccl-image-verifier-config] : %v", configInstance.ContainerClientConfig.CtrImageVerifierConfig.String()) } } diff --git a/containerm/daemon/daemon_internal.go b/containerm/daemon/daemon_internal.go index 39cf38e..48c2487 100644 --- a/containerm/daemon/daemon_internal.go +++ b/containerm/daemon/daemon_internal.go @@ -27,6 +27,11 @@ import ( func (d *daemon) start() error { log.Debug("starting daemon instance") + if err := d.startGrpcServers(); err != nil { + log.ErrorErr(err, "could not start gRPC servers") + return err + } + if err := d.loadContainerManagersStoredInfo(); err != nil { log.ErrorErr(err, "could not load and restore persistent data for the Container Manager Services") return err @@ -54,7 +59,7 @@ func (d *daemon) start() error { log.Debug("Containers Update Agent is not enabled.") } - return d.startGrpcServers() + return nil } func (d *daemon) stop() { diff --git a/containerm/daemon/daemon_test.go b/containerm/daemon/daemon_test.go index 937d270..046b2b4 100644 --- a/containerm/daemon/daemon_test.go +++ b/containerm/daemon/daemon_test.go @@ -16,6 +16,7 @@ import ( "fmt" "os" "reflect" + "strings" "testing" "time" @@ -83,43 +84,57 @@ func TestDefaultConfig(t *testing.T) { func TestThingsServiceFeaturesConfig(t *testing.T) { local := &config{} _ = loadLocalConfig("../pkg/testutil/config/daemon-things-features-config.json", local) - testutil.AssertEqual(t, local.ThingsConfig.Features, []string{things.ContainerFactoryFeatureID}) + testutil.AssertEqual(t, []string{things.ContainerFactoryFeatureID}, local.ThingsConfig.Features) } func TestThingsTLSConfig(t *testing.T) { local := &config{} _ = loadLocalConfig("../pkg/testutil/config/daemon-things-tls-config.json", local) - testutil.AssertEqual(t, local.ThingsConfig.ThingsConnectionConfig.Transport, &tlsConfig{RootCA: "ca.crt", ClientCert: "client.crt", ClientKey: "client.key"}) + testutil.AssertEqual(t, &tlsConfig{RootCA: "ca.crt", ClientCert: "client.crt", ClientKey: "client.key"}, local.ThingsConfig.ThingsConnectionConfig.Transport) +} + +func TestMgrDefaultCtrsStopTimeoutConfig(t *testing.T) { + local := &config{} + _ = loadLocalConfig("../pkg/testutil/config/daemon-mgr-default-ctrs-stop-timeout-config.json", local) + testutil.AssertEqual(t, local.ManagerConfig.MgrDefaultCtrsStopTimeout, "15") } func TestExtractOpts(t *testing.T) { t.Run("test_extract_ctr_client_opts", func(t *testing.T) { opts := extractCtrClientConfigOptions(cfg) - if opts == nil || len(opts) == 0 { + if len(opts) == 0 { t.Error("no ctr client opts after extraction") } }) t.Run("test_extract_net_mgr_opts", func(t *testing.T) { opts := extractNetManagerConfigOptions(cfg) - if opts == nil || len(opts) == 0 { + if len(opts) == 0 { t.Error("no net mgr opts after extraction") } }) t.Run("test_extract_ctr_mgr_opts", func(t *testing.T) { opts := extractContainerManagerOptions(cfg) - if opts == nil || len(opts) == 0 { + if len(opts) == 0 { + t.Error("no ctr mgr opts after extraction") + } + }) + t.Run("test_extract_ctr_mgr_opts_with_not_suffixed_stop_timeout", func(t *testing.T) { + config := &config{ManagerConfig: &managerConfig{MgrDefaultCtrsStopTimeout: "10"}} + opts := extractContainerManagerOptions(config) + if len(opts) == 0 { t.Error("no ctr mgr opts after extraction") } + testutil.AssertEqual(t, "10s", config.ManagerConfig.MgrDefaultCtrsStopTimeout) }) t.Run("test_extract_grpc_opts", func(t *testing.T) { opts := extractGrpcOptions(cfg) - if opts == nil || len(opts) == 0 { + if len(opts) == 0 { t.Error("no grpc opts after extraction") } }) t.Run("test_extract_things_opts", func(t *testing.T) { opts := extractThingsOptions(cfg) - if opts == nil || len(opts) == 0 { + if len(opts) == 0 { t.Error("no things opts after extraction") } }) @@ -215,7 +230,7 @@ func TestSetCommandFlags(t *testing.T) { }, "test_flags_cm-deflt-ctrs-stop-timeout": { flag: "cm-deflt-ctrs-stop-timeout", - expectedType: reflect.Int64.String(), + expectedType: reflect.String.String(), }, "test_flags_ccl-default-ns": { flag: "ccl-default-ns", @@ -261,6 +276,14 @@ func TestSetCommandFlags(t *testing.T) { flag: "ccl-lease-id", expectedType: reflect.String.String(), }, + "test_flags_ccl-image-verifier-type": { + flag: "ccl-image-verifier-type", + expectedType: reflect.String.String(), + }, + "test_flags_ccl-image-verifier-config": { + flag: "ccl-image-verifier-config", + expectedType: "stringSlice", + }, "test_flags_net-type": { flag: "net-type", expectedType: reflect.String.String(), @@ -508,15 +531,6 @@ func TestParseRegistryConfigs(t *testing.T) { t.Errorf("error while parsing registry configs, with nil insecure registries") } }) - t.Run("test_parse_registry_configs_null_insecure", func(t *testing.T) { - cfg := getDefaultInstance() - _ = loadLocalConfig(registriesDaemonConfig, cfg) - cfg.ContainerClientConfig.CtrInsecureRegistries = nil - registryConfigs := parseRegistryConfigs(cfg.ContainerClientConfig.CtrRegistryConfigs, cfg.ContainerClientConfig.CtrInsecureRegistries) - if registryConfigs == nil || len(registryConfigs) == 0 { - t.Errorf("error while parsing registry configs, with nil insecure registries") - } - }) t.Run("test_parse_registry_configs_null_registry_config", func(t *testing.T) { cfg := getDefaultInstance() _ = loadLocalConfig(registriesDaemonConfig, cfg) @@ -661,4 +675,66 @@ func TestRunLock(t *testing.T) { }) } +func TestImageVerifierConfig(t *testing.T) { + local := &config{} + _ = loadLocalConfig("../pkg/testutil/config/daemon-config-image-verifier.json", local) + testutil.AssertEqual(t, "notation", local.ContainerClientConfig.CtrImageVerifierType) + expected := map[string]string{"configDir": "/path/notation/config", "libexecDir": "/path/notation/libexec"} + assertStringMap(t, expected, local.ContainerClientConfig.CtrImageVerifierConfig) +} + +func TestImageVerifierFlag(t *testing.T) { + const ( + testSinglePair = "key=value" + testMultiplePairs = "key0=value0,key1=value1,key2=value2" + ) + + tests := map[string]struct { + value string + expectedValue map[string]string + expectedStringPairs []string + expectedErr error + }{ + "test_empty_error": { + expectedErr: log.NewError("the image verifier config could not be empty"), + }, + "test_parse_error": { + value: "invalid", + expectedErr: log.NewError("could not parse image verification config, invalid key-value pair - invalid"), + }, + "test_single_config_no_error": { + value: testSinglePair, + expectedStringPairs: []string{testSinglePair}, + expectedValue: map[string]string{"key": "value"}, + }, + "test_multiple_configs_no_error": { + value: testMultiplePairs, + expectedStringPairs: strings.Split(testMultiplePairs, ","), + expectedValue: map[string]string{"key0": "value0", "key1": "value1", "key2": "value2"}, + }, + } + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + verifierConfig := &verifierConfig{} + testutil.AssertError(t, test.expectedErr, verifierConfig.Set(test.value)) + for _, expectedPair := range test.expectedStringPairs { + testutil.AssertTrue(t, strings.Contains(verifierConfig.String(), expectedPair)) + } + testutil.AssertEqual(t, len(test.expectedValue), len(*verifierConfig)) + assertStringMap(t, test.expectedValue, *verifierConfig) + + }) + + } +} + +func assertStringMap(t *testing.T, expected, actual map[string]string) { + testutil.AssertEqual(t, len(expected), len(actual)) + for key, expectedValue := range expected { + value, ok := actual[key] + testutil.AssertTrue(t, ok) + testutil.AssertEqual(t, expectedValue, value) + } +} + // TODO test the behavior of the daemon towards its services (start, stop), with mocked instanced of GRPC service etc. diff --git a/containerm/deployment/deployment_internal.go b/containerm/deployment/deployment_internal.go index eb49c78..121fe8a 100644 --- a/containerm/deployment/deployment_internal.go +++ b/containerm/deployment/deployment_internal.go @@ -166,7 +166,7 @@ func stopContainer(ctx context.Context, ctrMgr mgr.ContainerManager, container * } func removeContainer(ctx context.Context, ctrMgr mgr.ContainerManager, container *types.Container) { - if removeErr := ctrMgr.Remove(ctx, container.ID, true); removeErr != nil { + if removeErr := ctrMgr.Remove(ctx, container.ID, true, nil); removeErr != nil { log.WarnErr(removeErr, "could not remove container with ID = %s, name = %s and image name = %s", container.ID, container.Name, container.Image.Name) } else { log.Debug("successfully removed container with ID = %s, name = %s and image name = %s", container.ID, container.Name, container.Image.Name) diff --git a/containerm/deployment/deployment_test.go b/containerm/deployment/deployment_test.go index e3461f3..102bbad 100755 --- a/containerm/deployment/deployment_test.go +++ b/containerm/deployment/deployment_test.go @@ -461,7 +461,7 @@ func TestUpdate(t *testing.T) { mockMgr.EXPECT().Create(testContext, testContainerMatcher).Return(newContainer, nil).Times(1) mockMgr.EXPECT().Stop(testContext, oldID, testStopOpts).Return(nil).Times(1) mockMgr.EXPECT().Start(testContext, newID).Return(nil).Times(1) - mockMgr.EXPECT().Remove(testContext, oldID, true).Do(func(ctx context.Context, ctrID string, force bool) { + mockMgr.EXPECT().Remove(testContext, oldID, true, nil).Do(func(ctx context.Context, ctrID string, force bool, stopOpts *types.StopOpts) { testWaitGroup.Done() }).Return(nil).Times(1) return nil @@ -482,7 +482,7 @@ func TestUpdate(t *testing.T) { mockMgr.EXPECT().List(testContext).Return([]*types.Container{testCtr}, nil) mockMgr.EXPECT().Create(testContext, testContainerMatcher).Return(newContainer, nil).Times(1) mockMgr.EXPECT().Start(testContext, newID).Return(nil).Times(1) - mockMgr.EXPECT().Remove(testContext, oldID, true).Do(func(ctx context.Context, ctrID string, force bool) { + mockMgr.EXPECT().Remove(testContext, oldID, true, nil).Do(func(ctx context.Context, ctrID string, force bool, stopOpts *types.StopOpts) { testWaitGroup.Done() }).Return(nil).Times(1) return nil @@ -519,7 +519,7 @@ func TestUpdate(t *testing.T) { mockMgr.EXPECT().Stop(testContext, oldID, testStopOpts).Return(nil).Times(1) mockMgr.EXPECT().Start(testContext, newID).Return(log.NewError("test error")).Times(1) mockMgr.EXPECT().Start(testContext, oldID).Return(nil).Times(1) - mockMgr.EXPECT().Remove(testContext, newID, true).Do(func(ctx context.Context, ctrID string, force bool) { + mockMgr.EXPECT().Remove(testContext, newID, true, nil).Do(func(ctx context.Context, ctrID string, force bool, stopOpts *types.StopOpts) { testWaitGroup.Done() }).Return(nil).Times(1) return nil @@ -542,7 +542,7 @@ func TestUpdate(t *testing.T) { mockMgr.EXPECT().Create(testContext, testContainerMatcher).Return(newContainer, nil).Times(1) mockMgr.EXPECT().Stop(testContext, oldID, testStopOpts).Return(log.NewError("test error")).Times(1) mockMgr.EXPECT().Start(testContext, newID).Return(log.NewError("test error")).Times(1) - mockMgr.EXPECT().Remove(testContext, newID, true).Do(func(ctx context.Context, ctrID string, force bool) { + mockMgr.EXPECT().Remove(testContext, newID, true, nil).Do(func(ctx context.Context, ctrID string, force bool, stopOpts *types.StopOpts) { testWaitGroup.Done() }).Return(nil).Times(1) return nil @@ -565,7 +565,7 @@ func TestUpdate(t *testing.T) { mockMgr.EXPECT().Create(testContext, testContainerMatcher).Return(newContainer, nil).Times(1) mockMgr.EXPECT().Stop(testContext, oldID, testStopOpts).Return(nil).Times(1) mockMgr.EXPECT().Start(testContext, newID).Return(nil).Times(1) - mockMgr.EXPECT().Remove(testContext, oldID, true).Do(func(ctx context.Context, ctrID string, force bool) { + mockMgr.EXPECT().Remove(testContext, oldID, true, nil).Do(func(ctx context.Context, ctrID string, force bool, stopOpts *types.StopOpts) { testWaitGroup.Done() }).Return(log.NewError("test error")).Times(1) return nil diff --git a/containerm/mgr/mgr.go b/containerm/mgr/mgr.go index a7cafa0..9b8b581 100644 --- a/containerm/mgr/mgr.go +++ b/containerm/mgr/mgr.go @@ -49,7 +49,7 @@ func init() { type containerMgr struct { metaPath string execPath string - defaultCtrsStopTimeout int64 + defaultCtrsStopTimeout time.Duration ctrClient ctr.ContainerAPIClient netMgr network.ContainerNetworkManager eventsMgr events.ContainerEventsManager @@ -269,7 +269,7 @@ func (mgr *containerMgr) Stop(ctx context.Context, id string, stopOpts *types.St } container.Lock() defer container.Unlock() - go mgr.applyRestartPolicy(context.Background(), container) + mgr.applyRestartPolicy(context.Background(), container) return nil } @@ -312,10 +312,11 @@ func (mgr *containerMgr) Update(ctx context.Context, id string, updateOpts *type changesMade = true } + var rpChanged bool if updateOpts.RestartPolicy != nil && !reflect.DeepEqual(updateOpts.RestartPolicy, container.HostConfig.RestartPolicy) { mgr.resetContainerRestartManager(container, false) container.HostConfig.RestartPolicy = updateOpts.RestartPolicy - changesMade = true + changesMade, rpChanged = true, true } if changesMade { @@ -327,6 +328,10 @@ func (mgr *containerMgr) Update(ctx context.Context, id string, updateOpts *type log.ErrorErr(errMeta, failedConfigStoringErrorMsg) } } + + if rpChanged && (container.State.Exited || container.State.Status == types.Stopped) { + mgr.applyRestartPolicy(context.Background(), container) + } return nil } @@ -418,7 +423,7 @@ func (mgr *containerMgr) Rename(ctx context.Context, id string, name string) err } // Remove removes a container, it may be running or stopped and so on. -func (mgr *containerMgr) Remove(ctx context.Context, id string, force bool) error { +func (mgr *containerMgr) Remove(ctx context.Context, id string, force bool, stopOpts *types.StopOpts) error { container := mgr.getContainerFromCache(id) if container == nil { return log.NewErrorf(noSuchContainerErrorMsg, id) @@ -439,7 +444,15 @@ func (mgr *containerMgr) Remove(ctx context.Context, id string, force bool) erro if (!util.IsContainerRunningOrPaused(container)) || force { mgr.cancelContainerRestartManager(container) - stopOpts := mgr.getContainerStopOptions(force) + if stopOpts == nil { + stopOpts = mgr.getContainerStopOptions(force) + } else { + mgr.fillContainerStopDefaults(stopOpts) + } + if err := util.ValidateStopOpts(stopOpts); err != nil { + log.ErrorErr(err, "invalid stop options for container id = %s", container.ID) + return err + } _, _, err := mgr.ctrClient.DestroyContainer(ctx, container, stopOpts, true) if err != nil && !(strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not found")) { diff --git a/containerm/mgr/mgr_api.go b/containerm/mgr/mgr_api.go index 595a47b..ea060ee 100644 --- a/containerm/mgr/mgr_api.go +++ b/containerm/mgr/mgr_api.go @@ -62,7 +62,7 @@ type ContainerManager interface { Rename(ctx context.Context, id string, name string) error // Remove removes a container, it may be running or stopped and so on - Remove(ctx context.Context, id string, force bool) error + Remove(ctx context.Context, id string, force bool, stopOpts *types.StopOpts) error // Metrics retrieves metrics data about a container Metrics(ctx context.Context, id string) (*types.Metrics, error) diff --git a/containerm/mgr/mgr_internal.go b/containerm/mgr/mgr_internal.go index 74437b5..9e3171e 100644 --- a/containerm/mgr/mgr_internal.go +++ b/containerm/mgr/mgr_internal.go @@ -96,7 +96,7 @@ func (mgr *containerMgr) applyRestartPolicy(ctx context.Context, container *type } } if err != nil { - mgr.updateConfigToStopped(ctx, container, -1, err) + mgr.updateConfigToStopped(ctx, container, -1, err, false) } }() } @@ -132,7 +132,7 @@ func (mgr *containerMgr) containersToArray() []*types.Container { } return ctrs } -func (mgr *containerMgr) updateConfigToStopped(ctx context.Context, c *types.Container, exitCode int64, err error) error { +func (mgr *containerMgr) updateConfigToStopped(ctx context.Context, c *types.Container, exitCode int64, err error, releaseContainerResources bool) error { var ( code int64 errMsg string @@ -155,7 +155,10 @@ func (mgr *containerMgr) updateConfigToStopped(ctx context.Context, c *types.Con } }() - return mgr.releaseContainerResources(c) + if releaseContainerResources { + return mgr.releaseContainerResources(c) + } + return nil } func (mgr *containerMgr) updateConfigToExited(ctx context.Context, c *types.Container, exitCode int64, err error, oomKilled bool) error { @@ -298,7 +301,7 @@ func (mgr *containerMgr) processStartContainer(ctx context.Context, id string, r pid, err = mgr.ctrClient.StartContainer(ctx, container, "") if err != nil { - _ = mgr.updateConfigToStopped(ctx, container, -1, err) + _ = mgr.updateConfigToStopped(ctx, container, -1, err, true) return err } @@ -361,7 +364,7 @@ func (mgr *containerMgr) stopContainer(ctx context.Context, container *types.Con container.ManuallyStopped = false return exitErr } - return mgr.updateConfigToStopped(ctx, container, exitCode, exitErr) + return mgr.updateConfigToStopped(ctx, container, exitCode, exitErr, true) } func (mgr *containerMgr) stopManagerService(ctx context.Context) error { @@ -387,10 +390,11 @@ func (mgr *containerMgr) stopManagerService(ctx context.Context) error { } return nil } + func (mgr *containerMgr) fillContainerStopDefaults(stopOpts *types.StopOpts) { if stopOpts != nil { if stopOpts.Timeout == 0 { - stopOpts.Timeout = mgr.defaultCtrsStopTimeout + stopOpts.Timeout = int64(mgr.defaultCtrsStopTimeout.Seconds()) } if stopOpts.Signal == "" { stopOpts.Signal = sigterm @@ -399,7 +403,7 @@ func (mgr *containerMgr) fillContainerStopDefaults(stopOpts *types.StopOpts) { } func (mgr *containerMgr) getContainerStopOptions(force bool) *types.StopOpts { return &types.StopOpts{ - Timeout: mgr.defaultCtrsStopTimeout, + Timeout: int64(mgr.defaultCtrsStopTimeout.Seconds()), Force: force, Signal: sigterm, } diff --git a/containerm/mgr/mgr_internal_init.go b/containerm/mgr/mgr_internal_init.go index 6341675..db9338e 100644 --- a/containerm/mgr/mgr_internal_init.go +++ b/containerm/mgr/mgr_internal_init.go @@ -14,6 +14,7 @@ package mgr import ( "fmt" + "time" "github.com/eclipse-kanto/container-management/containerm/containers/types" "github.com/eclipse-kanto/container-management/containerm/ctr" @@ -23,7 +24,7 @@ import ( "github.com/eclipse-kanto/container-management/containerm/util" ) -func newContainerMgr(metaPath string, execPath string, defaultCtrsStopTimeout int64, ctrClient ctr.ContainerAPIClient, netMgr network.ContainerNetworkManager, eventsMgr events.ContainerEventsManager) (ContainerManager, error) { +func newContainerMgr(metaPath string, execPath string, defaultCtrsStopTimeout time.Duration, ctrClient ctr.ContainerAPIClient, netMgr network.ContainerNetworkManager, eventsMgr events.ContainerEventsManager) (ContainerManager, error) { if err := util.MkDir(execPath); err != nil { return nil, err } @@ -46,6 +47,7 @@ func newContainerMgr(metaPath string, execPath string, defaultCtrsStopTimeout in restartCtrsMgrCache: newRestartMgrCache(), containerRepository: &ctrRepository, } + fmt.Println("mgr:", defaultCtrsStopTimeout.String()) ctrClient.SetContainerExitHooks(manager.exitedAndRelease) return manager, nil diff --git a/containerm/mgr/mgr_opts.go b/containerm/mgr/mgr_opts.go index 9021880..cb827b6 100644 --- a/containerm/mgr/mgr_opts.go +++ b/containerm/mgr/mgr_opts.go @@ -12,6 +12,12 @@ package mgr +import ( + "time" + + "github.com/eclipse-kanto/container-management/containerm/log" +) + // ContainerManagerOpt provides container manager options type ContainerManagerOpt func(mgrOptions *mgrOpts) error @@ -20,7 +26,7 @@ type mgrOpts struct { rootExec string containerClientServiceID string networkManagerServiceID string - defaultCtrsStopTimeout int64 + defaultCtrsStopTimeout time.Duration } func applyOptsMgr(mgrOpts *mgrOpts, opts ...ContainerManagerOpt) error { @@ -65,9 +71,16 @@ func WithMgrNetworkManagerServiceID(networkManagerServiceID string) ContainerMan } // WithMgrDefaultContainerStopTimeout sets default container stop timeout. -func WithMgrDefaultContainerStopTimeout(networkManagerCtrsStopTimeout int64) ContainerManagerOpt { +func WithMgrDefaultContainerStopTimeout(managerCtrsStopTimeout interface{}) ContainerManagerOpt { return func(mgrOptions *mgrOpts) error { - mgrOptions.defaultCtrsStopTimeout = networkManagerCtrsStopTimeout + switch v := managerCtrsStopTimeout.(type) { + case int64: + mgrOptions.defaultCtrsStopTimeout = time.Duration(managerCtrsStopTimeout.(int64)) * time.Second + case time.Duration: + mgrOptions.defaultCtrsStopTimeout = managerCtrsStopTimeout.(time.Duration) + default: + return log.NewErrorf("unexpected stop timeout type: %v", v) + } return nil } } diff --git a/containerm/mgr/mgr_opts_test.go b/containerm/mgr/mgr_opts_test.go index 80ff878..4d1b8bc 100644 --- a/containerm/mgr/mgr_opts_test.go +++ b/containerm/mgr/mgr_opts_test.go @@ -75,10 +75,22 @@ func TestMgrOpts(t *testing.T) { networkManagerServiceID: testNetworkManagerServiceID, }, }, - "test_mgs_default_container_stop_timeout": { - testOpt: WithMgrDefaultContainerStopTimeout(testDefaultContainerStopTimeout), + "test_mgr_default_container_stop_timeout": { + testOpt: WithMgrDefaultContainerStopTimeout(testContainerStopTimeout), expectedOpts: &mgrOpts{ - defaultCtrsStopTimeout: testDefaultContainerStopTimeout, + defaultCtrsStopTimeout: testContainerStopTimeout, + }, + }, + "test_mgr_default_container_stop_timeout_int64": { + testOpt: WithMgrDefaultContainerStopTimeout(int64(10)), + expectedOpts: &mgrOpts{ + defaultCtrsStopTimeout: testContainerStopTimeout, + }, + }, + "test_mgr_default_container_stop_timeout_string": { + testOpt: WithMgrDefaultContainerStopTimeout("10"), + expectedOpts: &mgrOpts{ + defaultCtrsStopTimeout: 0, }, }, } diff --git a/containerm/mgr/mgr_test.go b/containerm/mgr/mgr_test.go index afdcec7..3ab1086 100644 --- a/containerm/mgr/mgr_test.go +++ b/containerm/mgr/mgr_test.go @@ -42,8 +42,8 @@ const ( testRootExec = "testRootExec" testContainerClientServiceID = "testContainerClientServiceID" testNetworkManagerServiceID = "testNetworkManagerServiceID" - testDefaultContainerStopTimeout = 10 - testNetworkManagerStopTimeout = 30 + testContainerStopTimeout = time.Duration(10) * time.Second + testDefaultContainerStopTimeout = time.Duration(30) * time.Second ) func TestGetContainer(t *testing.T) { @@ -528,7 +528,7 @@ func TestDeleteContainerFromManager(t *testing.T) { testutil.AssertEqual(t, 1, len(containerCheck)) // Act - unitUnderTest.Remove(context.Background(), containerCheck[0].ID, true) + unitUnderTest.Remove(context.Background(), containerCheck[0].ID, true, &types.StopOpts{Force: true}) containerCheckAfter, err := unitUnderTest.List(context.Background()) testutil.AssertNil(t, err) @@ -1260,7 +1260,7 @@ func createContainerManagerWithCustomMocks( return containerMgr{ metaPath: metaPath, execPath: testRootExec, - defaultCtrsStopTimeout: testNetworkManagerStopTimeout, + defaultCtrsStopTimeout: testDefaultContainerStopTimeout, ctrClient: mockCtrClient, netMgr: mockNetworkManager, eventsMgr: mockEventsManager, diff --git a/containerm/pkg/testutil/config/daemon-config-image-verifier.json b/containerm/pkg/testutil/config/daemon-config-image-verifier.json new file mode 100644 index 0000000..04d24b9 --- /dev/null +++ b/containerm/pkg/testutil/config/daemon-config-image-verifier.json @@ -0,0 +1,95 @@ +{ + "log": { + "log_file": "log/container-management.log", + "log_level": "INFO", + "log_file_count": 5, + "log_file_size": 2, + "log_file_max_age": 28, + "syslog": false + }, + "manager": { + "home_dir": "/var/lib/container-management", + "exec_root_dir": "/var/run/container-management", + "container_client_sid": "container-management.service.local.v1.service-containerd-client", + "network_manager_sid": "container-management.service.local.v1.service-libnetwork-manager", + "default_ctrs_stop_timeout": 30 + }, + "containers": { + "default_ns": "kanto-cm", + "address_path": "/run/containerd/containerd.sock", + "insecure_registries": [ + "localhost" + ], + "exec_root_dir": "/var/run/container-management", + "home_dir": "/var/lib/container-management", + "runc_runtime": "io.containerd.runc.v2", + "image_expiry": "744h", + "image_expiry_disable": false, + "lease_id": "kanto-cm.lease", + "image_verifier_type": "notation", + "image_verifier_config": { + "configDir": "/path/notation/config", + "libexecDir": "/path/notation/libexec" + } + }, + "network": { + "type": "bridge", + "home_dir": "/var/lib/container-management", + "exec_root_dir": "/var/run/container-management", + "default_bridge": { + "name": "kanto-cm0", + "mtu": 1500, + "icc": true, + "ip_tables": true, + "ip_forward": true, + "ip_masq": true + } + }, + "grpc_server": { + "protocol": "unix", + "address_path": "/run/container-management/container-management.sock" + }, + "things": { + "enable": true, + "home_dir": "/var/lib/container-management", + "features": [ + "ContainerFactory", + "SoftwareUpdatable", + "Metrics" + ], + "connection": { + "broker_url": "tcp://localhost:1883", + "keep_alive": 20000, + "disconnect_timeout": 250, + "client_username": "", + "client_password": "", + "connect_timeout": 30000, + "acknowledge_timeout": 15000, + "subscribe_timeout": 15000, + "unsubscribe_timeout": 5000 + } + }, + "deployment": { + "enable": true, + "mode": "update", + "home_dir": "/var/lib/container-management", + "ctr_dir": "/etc/container-management/containers" + }, + "update_agent": { + "enable": false, + "domain": "containers", + "system_containers": [], + "verbose_inventory": false + }, + "connection": { + "broker_url": "tcp://localhost:1883", + "keep_alive": "20s", + "disconnect_timeout": "250ms", + "client_username": "", + "client_password": "", + "connect_timeout": "30s", + "acknowledge_timeout": "15s", + "subscribe_timeout": "15s", + "unsubscribe_timeout": "5s" + } +} diff --git a/containerm/pkg/testutil/config/daemon-config.json b/containerm/pkg/testutil/config/daemon-config.json index 55a1480..538e97b 100644 --- a/containerm/pkg/testutil/config/daemon-config.json +++ b/containerm/pkg/testutil/config/daemon-config.json @@ -12,7 +12,7 @@ "exec_root_dir": "/var/run/container-management", "container_client_sid": "container-management.service.local.v1.service-containerd-client", "network_manager_sid": "container-management.service.local.v1.service-libnetwork-manager", - "default_ctrs_stop_timeout": 30 + "default_ctrs_stop_timeout": "30s" }, "containers": { "default_ns": "kanto-cm", @@ -25,7 +25,8 @@ "runc_runtime": "io.containerd.runc.v2", "image_expiry": "744h", "image_expiry_disable": false, - "lease_id": "kanto-cm.lease" + "lease_id": "kanto-cm.lease", + "image_verifier_type": "none" }, "network": { "type": "bridge", diff --git a/containerm/pkg/testutil/config/daemon-mgr-default-ctrs-stop-timeout-config.json b/containerm/pkg/testutil/config/daemon-mgr-default-ctrs-stop-timeout-config.json new file mode 100644 index 0000000..cc49960 --- /dev/null +++ b/containerm/pkg/testutil/config/daemon-mgr-default-ctrs-stop-timeout-config.json @@ -0,0 +1,5 @@ +{ + "manager": { + "default_ctrs_stop_timeout": 15 + } +} \ No newline at end of file diff --git a/containerm/pkg/testutil/mocks/client/mock_client_api.go b/containerm/pkg/testutil/mocks/client/mock_client_api.go index c29b3c2..ffa9e9c 100644 --- a/containerm/pkg/testutil/mocks/client/mock_client_api.go +++ b/containerm/pkg/testutil/mocks/client/mock_client_api.go @@ -160,17 +160,17 @@ func (mr *MockClientMockRecorder) ProjectInfo(arg0 interface{}) *gomock.Call { } // Remove mocks base method. -func (m *MockClient) Remove(arg0 context.Context, arg1 string, arg2 bool) error { +func (m *MockClient) Remove(arg0 context.Context, arg1 string, arg2 bool, arg3 *types.StopOpts) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Remove", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Remove", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // Remove indicates an expected call of Remove. -func (mr *MockClientMockRecorder) Remove(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) Remove(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockClient)(nil).Remove), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockClient)(nil).Remove), arg0, arg1, arg2, arg3) } // Rename mocks base method. diff --git a/containerm/pkg/testutil/mocks/mgr/mock_mgr_api.go b/containerm/pkg/testutil/mocks/mgr/mock_mgr_api.go index 877c559..7308fa6 100644 --- a/containerm/pkg/testutil/mocks/mgr/mock_mgr_api.go +++ b/containerm/pkg/testutil/mocks/mgr/mock_mgr_api.go @@ -150,17 +150,17 @@ func (mr *MockContainerManagerMockRecorder) Pause(arg0, arg1 interface{}) *gomoc } // Remove mocks base method. -func (m *MockContainerManager) Remove(arg0 context.Context, arg1 string, arg2 bool) error { +func (m *MockContainerManager) Remove(arg0 context.Context, arg1 string, arg2 bool, arg3 *types.StopOpts) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Remove", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Remove", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // Remove indicates an expected call of Remove. -func (mr *MockContainerManagerMockRecorder) Remove(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockContainerManagerMockRecorder) Remove(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockContainerManager)(nil).Remove), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockContainerManager)(nil).Remove), arg0, arg1, arg2, arg3) } // Rename mocks base method. diff --git a/containerm/resources/container-management.service b/containerm/resources/container-management.service index 7b3c140..51a5f87 100644 --- a/containerm/resources/container-management.service +++ b/containerm/resources/container-management.service @@ -7,6 +7,8 @@ Requires=containerd.service [Service] Type=simple +Environment=HOME=%h +Environment=XDG_CONFIG_HOME=%E ExecStart=/usr/bin/container-management --cfg-file /etc/container-management/config.json Restart=always TimeoutSec=300 diff --git a/containerm/services/grpc_containers_service.go b/containerm/services/grpc_containers_service.go index 21f39dc..c840a8b 100644 --- a/containerm/services/grpc_containers_service.go +++ b/containerm/services/grpc_containers_service.go @@ -169,7 +169,7 @@ func (server *containers) Rename(ctx context.Context, request *pbcontainers.Rena } func (server *containers) Remove(ctx context.Context, request *pbcontainers.RemoveContainerRequest) (*empty.Empty, error) { - err := server.mgr.Remove(ctx, request.Id, request.Force) + err := server.mgr.Remove(ctx, request.Id, request.Force, protobuf.ToInternalStopOptions(request.StopOptions)) if err != nil { return nil, err } diff --git a/containerm/services/grpc_services_test.go b/containerm/services/grpc_services_test.go index 05c86b7..2e4834f 100644 --- a/containerm/services/grpc_services_test.go +++ b/containerm/services/grpc_services_test.go @@ -517,6 +517,20 @@ func TestRemove(t *testing.T) { }, mockExecution: mockExecRemoveErrors, }, + "test_remove_timeout": { + args: testRemoveArgs{ + ctx: testCtx, + request: &pbcontainers.RemoveContainerRequest{ + Id: containerID, + Force: true, + StopOptions: &pbcontainerstypes.StopOptions{ + Timeout: 20, + Force: true, + }, + }, + }, + mockExecution: mockExecRemoveNoErrors, + }, } // execute tests @@ -807,8 +821,7 @@ func mockExecStopNoErrors(args testStopArgs) (*empty.Empty, error) { } func mockExecStopDefaultOpts(args testStopArgs) (*empty.Empty, error) { - stopOpts := &types.StopOpts{} - mockContainerManager.EXPECT().Stop(args.ctx, args.request.Id, gomock.Eq(stopOpts)).Times(1).Return(nil) + mockContainerManager.EXPECT().Stop(args.ctx, args.request.Id, nil).Times(1).Return(nil) return &empty.Empty{}, nil } @@ -844,18 +857,20 @@ func mockExecUnpauseErrors(args testUnpauseArgs) (*empty.Empty, error) { // Remove ------------------------------------------------------------- func mockExecRemoveNoErrors(args testRemoveArgs) (*empty.Empty, error) { - mockContainerManager.EXPECT().Remove(args.ctx, args.request.Id, args.request.Force).Times(1).Return(nil) + mockContainerManager.EXPECT().Remove(args.ctx, args.request.Id, args.request.Force, gomock.Eq(protobuf.ToInternalStopOptions(args.request.StopOptions))).Times(1).Return(nil) return &empty.Empty{}, nil } func mockExecRemoveForce(args testRemoveArgs) (*empty.Empty, error) { - mockContainerManager.EXPECT().Remove(args.ctx, args.request.Id, true).Times(1).Return(nil) + mockContainerManager.EXPECT().Remove(args.ctx, args.request.Id, true, gomock.Eq(protobuf.ToInternalStopOptions(args.request.StopOptions))).Times(1).Return(nil) return &empty.Empty{}, nil } +//TODO add tests for timeout + func mockExecRemoveErrors(args testRemoveArgs) (*empty.Empty, error) { err := errors.New("failed to remove container") - mockContainerManager.EXPECT().Remove(args.ctx, args.request.Id, args.request.Force).Times(1).Return(err) + mockContainerManager.EXPECT().Remove(args.ctx, args.request.Id, args.request.Force, gomock.Eq(protobuf.ToInternalStopOptions(args.request.StopOptions))).Times(1).Return(err) return nil, err } diff --git a/containerm/things/features_container.go b/containerm/things/features_container.go index 8973694..dd066c1 100644 --- a/containerm/things/features_container.go +++ b/containerm/things/features_container.go @@ -152,7 +152,7 @@ func (ctrFeature *containerFeature) stopWithOptions(ctx context.Context, opts *s }) } func (ctrFeature *containerFeature) remove(ctx context.Context, force bool) error { - return ctrFeature.mgr.Remove(ctx, extractContainerID(ctrFeature.id), force) + return ctrFeature.mgr.Remove(ctx, extractContainerID(ctrFeature.id), force, nil) } func (ctrFeature *containerFeature) rename(ctx context.Context, name string) error { diff --git a/containerm/things/features_container_test.go b/containerm/things/features_container_test.go index a96a0fc..eeb9bed 100644 --- a/containerm/things/features_container_test.go +++ b/containerm/things/features_container_test.go @@ -272,7 +272,7 @@ func TestFeatureOperationsHandlerRemove(t *testing.T) { "test_feature_operations_handler_remove_no_errors": { opts: testValidUnmarshaled, mockExecution: func() error { - mockContainerManager.EXPECT().Remove(gomock.Any(), testContainerID, true).Times(1) + mockContainerManager.EXPECT().Remove(gomock.Any(), testContainerID, true, nil).Times(1) return nil }, }, @@ -296,7 +296,7 @@ func TestFeatureOperationsHandlerRemove(t *testing.T) { opts: testValidUnmarshaled, mockExecution: func() error { err := log.NewError("error while removing") - mockContainerManager.EXPECT().Remove(gomock.Any(), testContainerID, true).Times(1).Return(err) + mockContainerManager.EXPECT().Remove(gomock.Any(), testContainerID, true, nil).Times(1).Return(err) return err }, }, @@ -454,7 +454,7 @@ func TestFeatureOperationsUnsupportedOperation(t *testing.T) { mockContainerManager.EXPECT().Stop(gomock.Any(), testContainerID, gomock.Any()).Times(0) mockContainerManager.EXPECT().Pause(gomock.Any(), testContainerID).Times(0) mockContainerManager.EXPECT().Unpause(gomock.Any(), testContainerID).Times(0) - mockContainerManager.EXPECT().Remove(gomock.Any(), testContainerID, true).Times(0) + mockContainerManager.EXPECT().Remove(gomock.Any(), testContainerID, true, nil).Times(0) t.Run("test_feature_operations_handler_unsupported_operation", func(t *testing.T) { result, resultErr := containerFeature.featureOperationsHandler(testUnsupportedOperationName, nil) diff --git a/containerm/things/features_rollouts_software_updatable_internal.go b/containerm/things/features_rollouts_software_updatable_internal.go index a2d9983..cc50b51 100644 --- a/containerm/things/features_rollouts_software_updatable_internal.go +++ b/containerm/things/features_rollouts_software_updatable_internal.go @@ -258,7 +258,7 @@ func (su *softwareUpdatable) removeDependency(toRemove *datatypes.DependencyDesc log.Warn("container with ID = %s does not exist", toRemove.Name) err = log.NewErrorf("container with ID = %s does not exist", toRemove.Name) } else { - err = su.mgr.Remove(ctx, toRemove.Name, true) // TODO currently matching only on Name - the container id + err = su.mgr.Remove(ctx, toRemove.Name, true, nil) // TODO currently matching only on Name - the container id } return err } diff --git a/containerm/things/features_rollouts_software_updatable_internal_test.go b/containerm/things/features_rollouts_software_updatable_internal_test.go index 232ceca..51ed6da 100644 --- a/containerm/things/features_rollouts_software_updatable_internal_test.go +++ b/containerm/things/features_rollouts_software_updatable_internal_test.go @@ -414,7 +414,7 @@ func mockExecInstallErrorNoAtrifacts(t *testing.T) error { func mockExecRemoveNoDependencyDescription(t *testing.T) error { setupSUFeature(t) - mockContainerManager.EXPECT().Remove(gomock.Any(), gomock.Any(), gomock.Any()).Times(0).Return(nil) + mockContainerManager.EXPECT().Remove(gomock.Any(), gomock.Any(), gomock.Any(), nil).Times(0).Return(nil) mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), gomock.Any(), gomock.Any()).Times(0).Return(nil) return client.NewMessagesParameterInvalidError("there are no DependencyDescriptions to be removed") } @@ -466,14 +466,14 @@ func mockExecSURemoveForced(t *testing.T) error { gomock.InOrder( mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemoveStatusRemoving).Times(1).Return(nil), mockContainerManager.EXPECT().Get(gomock.Any(), testSoftwareName).Return(&types.Container{}, nil), - mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true).Return(nil), + mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true, nil).Return(nil), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemovingStatusRemoved).Times(1).Return(nil), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemoveStatusRemoving).Times(1).Return(nil), mockContainerManager.EXPECT().Get(gomock.Any(), testSoftwareName).Return(&types.Container{}, nil), - mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true).Return(log.NewErrorf(testOperationRemoveErrorWhileRemovingMessage)), + mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true, nil).Return(log.NewErrorf(testOperationRemoveErrorWhileRemovingMessage)), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemoveStatusRemoving).Times(1).Return(nil), mockContainerManager.EXPECT().Get(gomock.Any(), testSoftwareName).Return(&types.Container{}, nil), - mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true).Return(nil), + mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true, nil).Return(nil), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemovingStatusRemoved).Times(1).Return(nil), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastFailedOperationProperty, testOperationRemoveStatusRemovingFinishedError).Times(1).Return(nil), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemoveStatusRemovingFinishedError).Times(1).Return(nil), @@ -488,7 +488,7 @@ func mockExecSURemoveNoSuchContainer(t *testing.T) error { gomock.InOrder( mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemoveStatusRemoving).Times(1).Return(nil), mockContainerManager.EXPECT().Get(gomock.Any(), testSoftwareName).Return(nil, nil), - mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true).Times(0).Return(nil), + mockContainerManager.EXPECT().Remove(gomock.Any(), testSoftwareName, true, nil).Times(0).Return(nil), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastFailedOperationProperty, testOperationRemoveStatusRemovingFinishedErrorNoSuchContainer).Times(1).Return(nil), mockThing.EXPECT().SetFeatureProperty(testSUFeature.GetID(), testLastOperationProperty, testOperationRemoveStatusRemovingFinishedErrorNoSuchContainer).Times(1).Return(nil), ) diff --git a/containerm/updateagent/update_manager_test.go b/containerm/updateagent/update_manager_test.go index b627407..0c20bfa 100644 --- a/containerm/updateagent/update_manager_test.go +++ b/containerm/updateagent/update_manager_test.go @@ -300,8 +300,10 @@ func createSimpleContainer(name, version string) *ctrtypes.Container { ctr := &ctrtypes.Container{ Name: name, Image: ctrtypes.Image{Name: name + ":" + version}, + State: &ctrtypes.State{}, } util.FillDefaults(ctr) + util.SetContainerStatusRunning(ctr, 1234) return ctr } @@ -310,3 +312,7 @@ func createSimpleDesiredComponent(name, version string) *types.ComponentWithConf Component: types.Component{ID: name, Version: version}, } } + +func createActionComponent(component *types.ComponentWithConfig) *types.Component { + return &types.Component{ID: domainName + ":" + component.ID, Version: component.Version} +} diff --git a/containerm/updateagent/update_operation.go b/containerm/updateagent/update_operation.go index 0a53e46..43fcc95 100644 --- a/containerm/updateagent/update_operation.go +++ b/containerm/updateagent/update_operation.go @@ -267,11 +267,12 @@ func download(o *operation, baselineAction *baselineAction) { actions := baselineAction.actions for _, action := range actions { - if lastAction != nil { - o.updateBaselineActionStatus(baselineAction, types.BaselineStatusDownloading, lastAction, types.ActionStatusDownloadSuccess, lastActionMessage) - } - lastAction = action if action.actionType == util.ActionCreate || action.actionType == util.ActionRecreate { + if lastAction != nil { + lastAction.feedbackAction.Status = types.ActionStatusDownloadSuccess + lastAction.feedbackAction.Message = lastActionMessage + } + lastAction = action o.updateBaselineActionStatus(baselineAction, types.BaselineStatusDownloading, action, types.ActionStatusDownloading, action.feedbackAction.Message) log.Debug("new container %s to be created...", action.feedbackAction.Component.ID) if err := o.createContainer(action.desired); err != nil { @@ -279,8 +280,6 @@ func download(o *operation, baselineAction *baselineAction) { return } lastActionMessage = "New container created." - } else { - lastAction = nil } } } @@ -304,28 +303,29 @@ func update(o *operation, baselineAction *baselineAction) { actions := baselineAction.actions for _, action := range actions { + if action.actionType != util.ActionRecreate && action.actionType != util.ActionDestroy && action.actionType != util.ActionUpdate { + continue + } if lastAction != nil { - o.updateBaselineActionStatus(baselineAction, types.BaselineStatusUpdating, lastAction, types.ActionStatusUpdateSuccess, lastActionMessage) + lastAction.feedbackAction.Status = types.ActionStatusUpdateSuccess + lastAction.feedbackAction.Message = lastActionMessage } + lastAction = action log.Debug("container %s to be updated...", action.feedbackAction.Component.ID) - lastAction = action + o.updateBaselineActionStatus(baselineAction, types.BaselineStatusUpdating, action, types.ActionStatusUpdating, action.feedbackAction.Message) if action.actionType == util.ActionRecreate || action.actionType == util.ActionDestroy { - o.updateBaselineActionStatus(baselineAction, types.BaselineStatusUpdating, action, types.ActionStatusUpdating, action.feedbackAction.Message) if err := o.stopContainer(action.current); err != nil { lastActionErr = err return } lastActionMessage = "Old container instance is stopped." - } else if action.actionType == util.ActionUpdate { - o.updateBaselineActionStatus(baselineAction, types.BaselineStatusUpdating, action, types.ActionStatusUpdating, action.feedbackAction.Message) + } else { // action.actionType == util.ActionUpdate if err := o.updateContainer(action.current, action.desired); err != nil { lastActionErr = err return } lastActionMessage = "Container instance is updated with new configuration." - } else { - lastActionMessage = action.feedbackAction.Message } } } @@ -349,14 +349,18 @@ func activate(o *operation, baselineAction *baselineAction) { actions := baselineAction.actions for _, action := range actions { + if action.actionType == util.ActionDestroy { + continue + } if lastAction != nil { - o.updateBaselineActionStatus(baselineAction, types.BaselineStatusActivating, lastAction, types.ActionStatusActivationSuccess, lastActionMessage) + lastAction.feedbackAction.Status = types.ActionStatusActivationSuccess + lastAction.feedbackAction.Message = lastActionMessage } + lastAction = action log.Debug("container %s to be activated...", action.feedbackAction.Component.ID) - lastAction = action + o.updateBaselineActionStatus(baselineAction, types.BaselineStatusActivating, action, types.ActionStatusActivating, action.feedbackAction.Message) if action.actionType == util.ActionCheck || action.actionType == util.ActionUpdate { - o.updateBaselineActionStatus(baselineAction, types.BaselineStatusActivating, action, types.ActionStatusActivating, action.feedbackAction.Message) if err := o.ensureRunningContainer(action.current); err != nil { lastActionErr = err return @@ -367,14 +371,11 @@ func activate(o *operation, baselineAction *baselineAction) { lastActionMessage = action.feedbackAction.Message } } else if action.actionType == util.ActionCreate || action.actionType == util.ActionRecreate { - o.updateBaselineActionStatus(baselineAction, types.BaselineStatusActivating, action, types.ActionStatusActivating, action.feedbackAction.Message) if err := o.startContainer(action.desired); err != nil { lastActionErr = err return } lastActionMessage = "New container instance is started." - } else { - lastAction = nil } } } @@ -451,24 +452,41 @@ func cleanup(o *operation, baselineAction *baselineAction) { } else { delete(o.baselineActions, baseline) } - log.Debug("cleanup for baseline %s - starting...", baseline) - for _, action := range actions { - if action.actionType == util.ActionRecreate || action.actionType == util.ActionDestroy { - log.Debug("container %s to be cleanup...", action.feedbackAction.Component.ID) - err := o.removeContainer(action.current) - if action.actionType == util.ActionDestroy { - if err != nil { - action.feedbackAction.Status = types.ActionStatusRemovalFailure - action.feedbackAction.Message = err.Error() - } else { - action.feedbackAction.Status = types.ActionStatusRemovalSuccess - action.feedbackAction.Message = "Old container instance is removed." + + log.Debug("cleanup for baseline %s (%s) - starting...", baseline, baselineAction.status) + result := types.BaselineStatusCleanupSuccess + if baselineAction.status != types.BaselineStatusActivationSuccess { + log.Warn("cleanup implemented only for successfully activated baselines, no cleanup for baseline %s (%s)", baseline, baselineAction.status) + // TODO implement cleanup for failure scenarios, maybe together with rollback + } else { + for _, action := range actions { + if action.actionType == util.ActionRecreate || action.actionType == util.ActionDestroy { + log.Debug("container %s to be cleanup...", action.feedbackAction.Component.ID) + err := o.removeContainer(action.current) + if action.feedbackAction.Status == types.ActionStatusUpdateSuccess && action.actionType == util.ActionDestroy { + if err != nil { + action.feedbackAction.Status = types.ActionStatusRemovalFailure + action.feedbackAction.Message = err.Error() + result = types.BaselineStatusCleanupFailure + } else { + action.feedbackAction.Status = types.ActionStatusRemovalSuccess + action.feedbackAction.Message = "Old container instance is removed." + } } } } } - o.Feedback(types.BaselineStatusCleanupSuccess, "", baseline) - log.Debug("cleanup for baseline %s - done...", baseline) + o.Feedback(result, "", baseline) + log.Debug("cleanup for baseline (%s) %s - done...", baseline, baselineAction.status) + + if len(o.baselineActions) == 0 { + o.updateManager.operation = nil + if baselineAction.status == types.BaselineStatusActivationSuccess && result == types.BaselineStatusCleanupSuccess { + o.Feedback(types.StatusCompleted, "", "") + } else { + o.Feedback(types.StatusIncomplete, "", "") + } + } } func (o *operation) isSystemContainer(containerID string) bool { @@ -593,7 +611,7 @@ func (o *operation) ensureRunningContainer(current *ctrtypes.Container) error { func (o *operation) removeContainer(container *ctrtypes.Container) error { log.Debug("container [%s] is not desired - will be removed", container.Name) - if err := o.updateManager.mgr.Remove(o.ctx, container.ID, true); err != nil { + if err := o.updateManager.mgr.Remove(o.ctx, container.ID, true, nil); err != nil { log.ErrorErr(err, "could not remove undesired container [%s]", container.Name) return err } diff --git a/containerm/updateagent/update_operation_test.go b/containerm/updateagent/update_operation_test.go new file mode 100644 index 0000000..96e0e4d --- /dev/null +++ b/containerm/updateagent/update_operation_test.go @@ -0,0 +1,910 @@ +// Copyright (c) 2023 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +package updateagent + +import ( + "context" + "reflect" + "testing" + + ctrtypes "github.com/eclipse-kanto/container-management/containerm/containers/types" + "github.com/eclipse-kanto/container-management/containerm/pkg/testutil" + "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/matchers" + mgrmocks "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/mgr" + "github.com/eclipse-kanto/container-management/containerm/util" + "github.com/eclipse-kanto/update-manager/api/types" + ummocks "github.com/eclipse-kanto/update-manager/test/mocks" + "github.com/golang/mock/gomock" + "github.com/pkg/errors" +) + +var ( + stopOpts = &ctrtypes.StopOpts{ + Force: true, + Signal: "SIGTERM", + } +) + +type testContext struct { + activityID string + baseline string + desiredState *types.DesiredState + currentContainers []*ctrtypes.Container + desiredContainers []*ctrtypes.Container + actions []*types.Action + matchers []gomock.Matcher +} + +type testStep struct { + command types.CommandType + expect func(*testing.T, *mgrmocks.MockContainerManager, *ummocks.MockUpdateManagerCallback, *testContext) +} + +func TestExecute(t *testing.T) { + testCases := map[string]struct { + baseline string + steps []testStep + }{ + "test-execute-do-without-baseline": { + steps: []testStep{{command: types.CommandType("DO"), expect: nil}}, + }, + "test-execute-do-for-baseline": { + baseline: "test-baseline-0", + steps: []testStep{{command: types.CommandType("DO"), expect: nil}}, + }, + + "test-execute-without-baseline-no-errors": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: expectUpdateOK, + }, + { + command: types.CommandActivate, + expect: expectActivationOK, + }, + { + command: types.CommandCleanup, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 4, types.ActionStatusRemovalSuccess, "Old container instance is removed.") + gomock.InOrder( + // call to ContainerManager to remove old instance of test-container-2 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[1].ID, true, nil).Return(nil), + // call to ContainerManager to remove instance of test-container-5 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[3].ID, true, nil).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusCleanupSuccess, expActions1), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.StatusCompleted, expActions1), + ) + testctx.actions = expActions1 + }, + }, + }, + }, + + "test-execute-for-baselineA-no-errors": { + baseline: "test-baseline-a", + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusDownloadSuccess, "New container created.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[1]).Return(nil, nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadSuccess, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandUpdate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusUpdateSuccess, "Old container instance is stopped.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // call to ContainerManager to stop test-container-2 is expected + mockStopContainer(mockContainerManager, testctx.currentContainers[1]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateSuccess, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandActivate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 0, types.ActionStatusActivating, "") + expActions2 := copyAndUpdateActions(expActions1, 0, types.ActionStatusActivationSuccess, "Existing container instance is running.") + expActions2[1].Status = types.ActionStatusActivating + expActions3 := copyAndUpdateActions(expActions2, 1, types.ActionStatusActivationSuccess, "New container instance is started.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions1), + // call to ContainerManager to retrieve test-container-1 (state paused) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[0].ID).Return(testctx.currentContainers[0], nil), + // call to ContainerManager to unpause test-container-1 + mockUnpauseContainer(mockContainerManager, testctx.currentContainers[0]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions2), + // call to ContainerManager to start new test-container-2 + mockContainerManager.EXPECT().Start(context.Background(), gomock.Not(testctx.currentContainers[1].ID)).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationSuccess, expActions3), + ) + testctx.actions = expActions3 + }, + }, + { + command: types.CommandCleanup, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + gomock.InOrder( + // call to ContainerManager to remove old instance of test-container-2 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[1].ID, true, nil).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusCleanupSuccess, testctx.actions), + ) + }, + }, + }, + }, + + "test-execute-for-baselineB-no-errors": { + baseline: "test-baseline-b", + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 3, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 3, types.ActionStatusDownloadSuccess, "New container created.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[3]).Return(nil, nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadSuccess, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandUpdate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 2, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 2, types.ActionStatusUpdateSuccess, "Container instance is updated with new configuration.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // call to ContainerManager to update test-container-3 with new restart policy + mockContainerManager.EXPECT().Update(context.Background(), testctx.currentContainers[2].ID, gomock.Any()).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateSuccess, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandActivate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 2, types.ActionStatusActivating, "") + expActions2 := copyAndUpdateActions(expActions1, 2, types.ActionStatusActivationSuccess, "") + expActions2[3].Status = types.ActionStatusActivating + expActions3 := copyAndUpdateActions(expActions2, 3, types.ActionStatusActivationSuccess, "New container instance is started.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions1), + // call to ContainerManager to retrieve test-container-3 (state running) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[2].ID).Return(testctx.currentContainers[2], nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions2), + // call to ContainerManager to start test-container-4 + mockContainerManager.EXPECT().Start(context.Background(), testctx.desiredContainers[3].ID).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationSuccess, expActions3), + ) + testctx.actions = expActions3 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupNoActions, + }, + }, + }, + + "test-execute-for-baseline-destroy-no-errors": { + baseline: "containers:remove-components", + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadSuccess, testctx.actions) + }, + }, + { + command: types.CommandUpdate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 4, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 4, types.ActionStatusUpdateSuccess, "Old container instance is stopped.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // no stop call to ContainerManager as test-container-4 is not running (test setup) + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateSuccess, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandActivate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationSuccess, testctx.actions) + }, + }, + { + command: types.CommandCleanup, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 4, types.ActionStatusRemovalSuccess, "Old container instance is removed.") + gomock.InOrder( + // call to ContainerManager to remove instance of test-container-5 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[3].ID, true, nil).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusCleanupSuccess, expActions1), + ) + testctx.actions = expActions1 + }, + }, + }, + }, + + "test-execute-without-baseline-download-error-1": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusDownloadFailure, "cannot download container image") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[1]).Return(nil, errors.New("cannot download container image")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadFailure, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + "test-execute-without-baseline-download-error-2": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusDownloadSuccess, "New container created.") + expActions2[3].Status = types.ActionStatusDownloading + expActions3 := copyAndUpdateActions(expActions2, 3, types.ActionStatusDownloadFailure, "cannot download container image") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[1]).Return(nil, nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions2), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[3]).Return(nil, errors.New("cannot download container image")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadFailure, expActions3), + ) + testctx.actions = expActions3 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + "test-execute-for-baselineA-download-error": { + baseline: "test-baseline-a", + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusDownloadFailure, "cannot download container image") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[1]).Return(nil, errors.New("cannot download container image")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadFailure, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupNoActions, + }, + }, + }, + + "test-execute-without-baseline-update-error-1": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusUpdateFailure, "cannot stop container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // call to ContainerManager to stop test-container-2 is expected + mockContainerManager.EXPECT().Stop(context.Background(), testctx.currentContainers[1].ID, stopOpts).Return(errors.New("cannot stop container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateFailure, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + "test-execute-without-baseline-update-error-2": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusUpdateSuccess, "Old container instance is stopped.") + expActions2[2].Status = types.ActionStatusUpdating + expActions3 := copyAndUpdateActions(expActions2, 2, types.ActionStatusUpdateFailure, "cannot update container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // call to ContainerManager to stop test-container-2 is expected + mockStopContainer(mockContainerManager, testctx.currentContainers[1]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions2), + // call to ContainerManager to update test-container-3 with new restart policy + mockContainerManager.EXPECT().Update(context.Background(), testctx.currentContainers[2].ID, gomock.Any()).Return(errors.New("cannot update container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateFailure, expActions3), + ) + testctx.actions = expActions3 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + "test-execute-for-baselineA-update-error": { + baseline: "test-baseline-a", + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusDownloadSuccess, "New container created.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[1]).Return(nil, nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadSuccess, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandUpdate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusUpdateFailure, "cannot stop container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // call to ContainerManager to stop test-container-2 is expected + mockContainerManager.EXPECT().Stop(context.Background(), testctx.currentContainers[1].ID, stopOpts).Return(errors.New("cannot stop container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateFailure, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupNoActions, + }, + }, + }, + "test-execute-for-baselineB-update-error": { + baseline: "test-baseline-b", + steps: []testStep{ + { + command: types.CommandDownload, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 3, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 3, types.ActionStatusDownloadSuccess, "New container created.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[3]).Return(nil, nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadSuccess, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandUpdate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 2, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 2, types.ActionStatusUpdateFailure, "cannot update container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // call to ContainerManager to update test-container-3 with new restart policy + mockContainerManager.EXPECT().Update(context.Background(), testctx.currentContainers[2].ID, gomock.Any()).Return(errors.New("cannot update container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateFailure, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupNoActions, + }, + }, + }, + + "test-execute-without-baseline-activate-error-1": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: expectUpdateOK, + }, + { + command: types.CommandActivate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 0, types.ActionStatusActivating, "") + expActions2 := copyAndUpdateActions(expActions1, 0, types.ActionStatusActivationFailure, "cannot unpause container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions1), + // call to ContainerManager to retrieve test-container-1 (state paused) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[0].ID).Return(testctx.currentContainers[0], nil), + // call to ContainerManager to unpause test-container-1 + mockContainerManager.EXPECT().Unpause(context.Background(), testctx.currentContainers[0].ID).Return(errors.New("cannot unpause container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationFailure, expActions2), + ) + testctx.actions = expActions2 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + "test-execute-without-baseline-activate-error-2": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: expectUpdateOK, + }, + { + command: types.CommandActivate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 0, types.ActionStatusActivating, "") + expActions2 := copyAndUpdateActions(expActions1, 0, types.ActionStatusActivationSuccess, "Existing container instance is running.") + expActions2[1].Status = types.ActionStatusActivating + expActions3 := copyAndUpdateActions(expActions2, 1, types.ActionStatusActivationFailure, "cannot start new container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions1), + // call to ContainerManager to retrieve test-container-1 (state paused) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[0].ID).Return(testctx.currentContainers[0], nil), + // call to ContainerManager to unpause test-container-1 + mockUnpauseContainer(mockContainerManager, testctx.currentContainers[0]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions2), + // call to ContainerManager to start new test-container-2 + mockContainerManager.EXPECT().Start(context.Background(), gomock.Not(testctx.currentContainers[1].ID)).Return(errors.New("cannot start new container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationFailure, expActions3), + ) + testctx.actions = expActions3 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + "test-execute-without-baseline-activate-error-3": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: expectUpdateOK, + }, + { + command: types.CommandActivate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 0, types.ActionStatusActivating, "") + expActions2 := copyAndUpdateActions(expActions1, 0, types.ActionStatusActivationSuccess, "Existing container instance is running.") + expActions2[1].Status = types.ActionStatusActivating + expActions3 := copyAndUpdateActions(expActions2, 1, types.ActionStatusActivationSuccess, "New container instance is started.") + expActions3[2].Status = types.ActionStatusActivating + expActions4 := copyAndUpdateActions(expActions3, 2, types.ActionStatusActivationFailure, "cannot get current state for container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions1), + // call to ContainerManager to retrieve test-container-1 (state paused) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[0].ID).Return(testctx.currentContainers[0], nil), + // call to ContainerManager to unpause test-container-1 + mockUnpauseContainer(mockContainerManager, testctx.currentContainers[0]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions2), + // call to ContainerManager to start new test-container-2 + mockContainerManager.EXPECT().Start(context.Background(), gomock.Not(testctx.currentContainers[1].ID)).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions3), + // call to ContainerManager to retrieve test-container-3 (state running) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[2].ID).Return(nil, errors.New("cannot get current state for container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationFailure, expActions4), + ) + testctx.actions = expActions4 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + "test-execute-without-baseline-activate-error-4": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: expectUpdateOK, + }, + { + command: types.CommandActivate, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 0, types.ActionStatusActivating, "") + expActions2 := copyAndUpdateActions(expActions1, 0, types.ActionStatusActivationSuccess, "Existing container instance is running.") + expActions2[1].Status = types.ActionStatusActivating + expActions3 := copyAndUpdateActions(expActions2, 1, types.ActionStatusActivationSuccess, "New container instance is started.") + expActions3[2].Status = types.ActionStatusActivating + expActions4 := copyAndUpdateActions(expActions3, 2, types.ActionStatusActivationSuccess, "") + expActions4[3].Status = types.ActionStatusActivating + expActions5 := copyAndUpdateActions(expActions4, 3, types.ActionStatusActivationFailure, "cannot start new container instance") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions1), + // call to ContainerManager to retrieve test-container-1 (state paused) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[0].ID).Return(testctx.currentContainers[0], nil), + // call to ContainerManager to unpause test-container-1 + mockUnpauseContainer(mockContainerManager, testctx.currentContainers[0]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions2), + // call to ContainerManager to start new test-container-2 + mockContainerManager.EXPECT().Start(context.Background(), gomock.Not(testctx.currentContainers[1].ID)).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions3), + // call to ContainerManager to retrieve test-container-3 (state running) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[2].ID).Return(testctx.currentContainers[2], nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions4), + // call to ContainerManager to start test-container-4 + mockContainerManager.EXPECT().Start(context.Background(), testctx.desiredContainers[3].ID).Return(errors.New("cannot start new container instance")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationFailure, expActions5), + ) + testctx.actions = expActions5 + }, + }, + { + command: types.CommandCleanup, + expect: expectCleanupIncomplete, + }, + }, + }, + + "test-execute-without-baseline-cleanup-error-1": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: expectUpdateOK, + }, + { + command: types.CommandActivate, + expect: expectActivationOK, + }, + { + command: types.CommandCleanup, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 4, types.ActionStatusRemovalSuccess, "Old container instance is removed.") + gomock.InOrder( + // call to ContainerManager to remove old instance of test-container-2 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[1].ID, true, nil).Return(errors.New("cannot remove old container instance")), + // call to ContainerManager to remove instance of test-container-5 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[3].ID, true, nil).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusCleanupSuccess, expActions1), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.StatusCompleted, expActions1), + ) + testctx.actions = expActions1 + }, + }, + }, + }, + "test-execute-without-baseline-cleanup-error-2": { + steps: []testStep{ + { + command: types.CommandDownload, + expect: expectDownloadOK, + }, + { + command: types.CommandUpdate, + expect: expectUpdateOK, + }, + { + command: types.CommandActivate, + expect: expectActivationOK, + }, + { + command: types.CommandCleanup, + expect: func(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 4, types.ActionStatusRemovalFailure, "old container instance cannot be removed") + gomock.InOrder( + // call to ContainerManager to remove old instance of test-container-2 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[1].ID, true, nil).Return(nil), + // call to ContainerManager to remove instance of test-container-5 + mockContainerManager.EXPECT().Remove(context.Background(), testctx.currentContainers[3].ID, true, nil).Return(errors.New("old container instance cannot be removed")), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusCleanupFailure, expActions1), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.StatusIncomplete, expActions1), + ) + testctx.actions = expActions1 + }, + }, + }, + }, + } + mockCtr := gomock.NewController(t) + defer mockCtr.Finish() + + for testActivityID, testCase := range testCases { + t.Run(testActivityID, func(t *testing.T) { + mockContainerManager := mgrmocks.NewMockContainerManager(mockCtr) + updateManager := newUpdateManager(mockContainerManager, nil, domainName, []string{sysContainerName}, false) + ctrUpdManager := updateManager.(*containersUpdateManager) + mockCallback := ummocks.NewMockUpdateManagerCallback(mockCtr) + updateManager.SetCallback(mockCallback) + + // setup mocks + testctx := setupTestEnvironment(testActivityID, testCase.baseline) + mockContainerManager.EXPECT().List(gomock.Any()).Return(testctx.currentContainers, nil) + expectFeedback(t, mockCallback, testActivityID, "", types.StatusIdentifying, nil) + expectFeedback(t, mockCallback, testActivityID, "", types.StatusIdentified, testctx.actions) + + // perform identification before commands + updateManager.Apply(context.Background(), testActivityID, testctx.desiredState) + testutil.AssertNotNil(t, ctrUpdManager.operation) + operation := ctrUpdManager.operation.(*operation) + testutil.AssertEqual(t, testActivityID, operation.GetActivityID()) + testctx.desiredContainers = operation.desiredState.containers + + for _, step := range testCase.steps { + t.Logf("executing command %s for baseline '%s'", step.command, testCase.baseline) + if step.expect != nil { + step.expect(t, mockContainerManager, mockCallback, testctx) + } + operation.Execute(step.command, testCase.baseline) + } + }) + } +} + +// test setup: +// current containers: test-container-1 (paused), test-container-2, test-container-3, test-container-5 (stopped). note: test-container-4 is missing. +// desired state: +// - containers: test-container-1 (same), test-container-2 (new image), test-container-3 (config update), test-container-4 (new) +// - baselines: test-baseline-a (test-container-1, test-container-2), test-baseline-b (test-container-3, test-container-5) +// actions: test-container-1 (check), test-container-2 (recreate), test-container-3 (update), test-container-4 (create), test-container-5 (destroy) +func setupTestEnvironment(activityID, baseline string) *testContext { + component1 := createSimpleDesiredComponent("test-container-1", "1.1.1") + component2 := createSimpleDesiredComponent("test-container-2", "2.2.22") + component3 := createSimpleDesiredComponent("test-container-3", "3.3.3") + component3.Config = append(component3.Config, &types.KeyValuePair{Key: "restartPolicy", Value: "no"}) + component4 := createSimpleDesiredComponent("test-container-4", "4.4.4") + + container1 := createSimpleContainer("test-container-1", "1.1.1") + util.SetContainerStatusPaused(container1) + container2 := createSimpleContainer("test-container-2", "2.2.2") + container3 := createSimpleContainer("test-container-3", "3.3.3") + container5 := createSimpleContainer("test-container-5", "5.5.5") + util.SetContainerStatusStopped(container5, 0, "") + + return &testContext{ + activityID: activityID, + baseline: baseline, + desiredState: &types.DesiredState{ + Baselines: []*types.Baseline{ + {Title: "test-baseline-a", Components: []string{domainName + ":" + component1.ID, domainName + ":" + component2.ID}}, + {Title: "test-baseline-b", Components: []string{domainName + ":" + component3.ID, domainName + ":" + component4.ID}}, + }, + Domains: []*types.Domain{{ + ID: domainName, + Components: []*types.ComponentWithConfig{component1, component2, component3, component4}, + }}, + }, + currentContainers: []*ctrtypes.Container{container1, container2, container3, container5}, + actions: []*types.Action{ + { + Component: createActionComponent(component1), + Status: types.ActionStatusIdentified, + Message: util.GetActionMessage(util.ActionCheck), + }, + { + Component: createActionComponent(component2), + Status: types.ActionStatusIdentified, + Message: util.GetActionMessage(util.ActionRecreate), + }, + { + Component: createActionComponent(component3), + Status: types.ActionStatusIdentified, + Message: util.GetActionMessage(util.ActionUpdate), + }, + { + Component: createActionComponent(component4), + Status: types.ActionStatusIdentified, + Message: util.GetActionMessage(util.ActionCreate), + }, + { + Component: &types.Component{ID: domainName + ":" + container5.Name, Version: "5.5.5"}, + Status: types.ActionStatusIdentified, + Message: util.GetActionMessage(util.ActionDestroy), + }}, + matchers: []gomock.Matcher{ + matchers.MatchesContainerImage(component1.ID, component1.ID+":"+component1.Version), + matchers.MatchesContainerImage(component2.ID, component2.ID+":"+component2.Version), + matchers.MatchesContainerImage(component3.ID, component3.ID+":"+component3.Version), + matchers.MatchesContainerImage(component4.ID, component4.ID+":"+component4.Version), + }, + } +} + +func copyAndUpdateActions(actions []*types.Action, index int, status types.ActionStatusType, message string) []*types.Action { + copy := make([]*types.Action, len(actions)) + for i, action := range actions { + copy[i] = &types.Action{ + Component: action.Component, + Status: action.Status, + Progress: action.Progress, + Message: action.Message, + } + } + copy[index].Status = status + if message != "" { + copy[index].Message = message + } + return copy +} + +func expectFeedback(t *testing.T, mockCallback *ummocks.MockUpdateManagerCallback, + expActivityID, expBaseline string, expStatus types.StatusType, expActions []*types.Action) *gomock.Call { + return mockCallback.EXPECT().HandleDesiredStateFeedbackEvent(domainName, expActivityID, expBaseline, gomock.Any(), "", gomock.Any()).Do( + func(domain, activityID, baseline string, status types.StatusType, message string, actions []*types.Action) { + testutil.AssertEqual(t, expStatus, status) + testutil.AssertEqual(t, len(expActions), len(actions)) + for i, action := range actions { + expected := expActions[i] + if !reflect.DeepEqual(expected, action) { + t.Errorf("feedback action %d: expected (%s:%s, %s, %s, %d), got (%s:%s, %s, %s, %d)", i, + expected.Component.ID, expected.Component.Version, expected.Status, expected.Message, expected.Progress, + action.Component.ID, action.Component.Version, action.Status, action.Message, action.Progress) + } + } + }) +} + +func expectDownloadOK(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusDownloading, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusDownloadSuccess, "New container created.") + expActions2[3].Status = types.ActionStatusDownloading + expActions3 := copyAndUpdateActions(expActions2, 3, types.ActionStatusDownloadSuccess, "New container created.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions1), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[1]).Return(nil, nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloading, expActions2), + mockContainerManager.EXPECT().Create(context.Background(), testctx.matchers[3]).Return(nil, nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusDownloadSuccess, expActions3), + ) + testctx.actions = expActions3 +} + +func expectUpdateOK(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 1, types.ActionStatusUpdating, "") + expActions2 := copyAndUpdateActions(expActions1, 1, types.ActionStatusUpdateSuccess, "Old container instance is stopped.") + expActions2[2].Status = types.ActionStatusUpdating + expActions3 := copyAndUpdateActions(expActions2, 2, types.ActionStatusUpdateSuccess, "Container instance is updated with new configuration.") + expActions3[4].Status = types.ActionStatusUpdating + expActions4 := copyAndUpdateActions(expActions3, 4, types.ActionStatusUpdateSuccess, "Old container instance is stopped.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions1), + // call to ContainerManager to stop test-container-2 is expected + mockStopContainer(mockContainerManager, testctx.currentContainers[1]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions2), + // call to ContainerManager to update test-container-3 with new restart policy + mockContainerManager.EXPECT().Update(context.Background(), testctx.currentContainers[2].ID, gomock.Any()).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdating, expActions3), + // no stop call to ContainerManager as test-container-4 is not running (test setup) + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusUpdateSuccess, expActions4), + ) + testctx.actions = expActions4 +} + +func expectActivationOK(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expActions1 := copyAndUpdateActions(testctx.actions, 0, types.ActionStatusActivating, "") + expActions2 := copyAndUpdateActions(expActions1, 0, types.ActionStatusActivationSuccess, "Existing container instance is running.") + expActions2[1].Status = types.ActionStatusActivating + expActions3 := copyAndUpdateActions(expActions2, 1, types.ActionStatusActivationSuccess, "New container instance is started.") + expActions3[2].Status = types.ActionStatusActivating + expActions4 := copyAndUpdateActions(expActions3, 2, types.ActionStatusActivationSuccess, "") + expActions4[3].Status = types.ActionStatusActivating + expActions5 := copyAndUpdateActions(expActions4, 3, types.ActionStatusActivationSuccess, "New container instance is started.") + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions1), + // call to ContainerManager to retrieve test-container-1 (state paused) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[0].ID).Return(testctx.currentContainers[0], nil), + // call to ContainerManager to unpause test-container-1 + mockUnpauseContainer(mockContainerManager, testctx.currentContainers[0]), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions2), + // call to ContainerManager to start new test-container-2 + mockContainerManager.EXPECT().Start(context.Background(), gomock.Not(testctx.currentContainers[1].ID)).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions3), + // call to ContainerManager to retrieve test-container-3 (state running) + mockContainerManager.EXPECT().Get(context.Background(), testctx.currentContainers[2].ID).Return(testctx.currentContainers[2], nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivating, expActions4), + // call to ContainerManager to start test-container-4 + mockContainerManager.EXPECT().Start(context.Background(), testctx.desiredContainers[3].ID).Return(nil), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusActivationSuccess, expActions5), + ) + testctx.actions = expActions5 +} + +func mockUnpauseContainer(mockContainerManager *mgrmocks.MockContainerManager, container *ctrtypes.Container) *gomock.Call { + return mockContainerManager.EXPECT().Unpause(context.Background(), container.ID).DoAndReturn( + func(ctx context.Context, id string) error { + util.SetContainerStatusRunning(container, 5678) + return nil + }) +} + +func mockStopContainer(mockContainerManager *mgrmocks.MockContainerManager, container *ctrtypes.Container) *gomock.Call { + return mockContainerManager.EXPECT().Stop(context.Background(), container.ID, stopOpts).DoAndReturn( + func(ctx context.Context, id string, stopOpts *ctrtypes.StopOpts) error { + util.SetContainerStatusStopped(container, 9999, "stopped by update") + return nil + }) +} + +func expectCleanupNoActions(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusCleanupSuccess, testctx.actions) +} + +func expectCleanupIncomplete(t *testing.T, mockContainerManager *mgrmocks.MockContainerManager, mockCallback *ummocks.MockUpdateManagerCallback, testctx *testContext) { + gomock.InOrder( + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.BaselineStatusCleanupSuccess, testctx.actions), + expectFeedback(t, mockCallback, testctx.activityID, testctx.baseline, types.StatusIncomplete, testctx.actions), + ) +} diff --git a/containerm/util/cli/args_util.go b/containerm/util/cli/args_util.go index 9382cef..c2dbd8f 100644 --- a/containerm/util/cli/args_util.go +++ b/containerm/util/cli/args_util.go @@ -32,7 +32,7 @@ func ValidateContainerByNameArgsSingle(ctx context.Context, args []string, provi err error ) // parse parameters - if len(args) == 1 { + if len(args) >= 1 { if container, err = kantoCMClient.Get(ctx, args[0]); err != nil { return nil, err } else if container == nil { diff --git a/containerm/util/error/error_util.go b/containerm/util/error/error_util.go index 60e451a..abd9809 100644 --- a/containerm/util/error/error_util.go +++ b/containerm/util/error/error_util.go @@ -34,6 +34,11 @@ func (m *CompoundError) Size() int { // Error returns the combined error messages. func (m *CompoundError) Error() string { + return m.ErrorWithMessage(fmt.Sprintf("%d errors:", len(m.errs))) +} + +// ErrorWithMessage returns combined error messages with a custom message. +func (m *CompoundError) ErrorWithMessage(message string) string { if len(m.errs) == 0 { return fmt.Sprintf("no error") } @@ -46,5 +51,6 @@ func (m *CompoundError) Error() string { for i, err := range m.errs { serrs[i] = fmt.Sprintf("* %s", err) } - return fmt.Sprintf("%d errors:\n\n%s", len(m.errs), strings.Join(serrs, "\n")) + + return fmt.Sprintf("%s\n\n%s", message, strings.Join(serrs, "\n")) } diff --git a/containerm/util/protobuf/convert_test.go b/containerm/util/protobuf/convert_test.go index 5f2505f..af574aa 100644 --- a/containerm/util/protobuf/convert_test.go +++ b/containerm/util/protobuf/convert_test.go @@ -204,9 +204,7 @@ func TestConvertContainer(t *testing.T) { util.SetContainerStatusCreated(ctr) t.Run("test_convert_container", func(t *testing.T) { - protoCtr := ToProtoContainer(ctr) - internalCtr := ToInternalContainer(protoCtr) - testutil.AssertEqual(t, ctr, internalCtr) + testutil.AssertEqual(t, ctr, ToInternalContainer(ToProtoContainer(ctr))) }) } @@ -215,9 +213,7 @@ func TestConvertContainerEmpty(t *testing.T) { Image: internalImage, } t.Run("test_convert_container_empty", func(t *testing.T) { - protoCtr := ToProtoContainer(ctr) - internalCtr := ToInternalContainer(protoCtr) - testutil.AssertEqual(t, ctr, internalCtr) + testutil.AssertEqual(t, ctr, ToInternalContainer(ToProtoContainer(ctr))) }) } @@ -330,12 +326,15 @@ func TestToInternalStopOpts(t *testing.T) { stopOpts := &internaltypes.StopOpts{ Timeout: 20, Force: true, + Signal: "SIGTERM", } t.Run("test_convert_stop_options", func(t *testing.T) { - protoStopOpts := ToProtoStopOptions(stopOpts) - internalStopOpts := ToInternalStopOptions(protoStopOpts) - testutil.AssertEqual(t, stopOpts, internalStopOpts) + testutil.AssertEqual(t, stopOpts, ToInternalStopOptions(ToProtoStopOptions(stopOpts))) + }) + + t.Run("test_convert_stop_options_nil", func(t *testing.T) { + testutil.AssertNil(t, ToInternalStopOptions(ToProtoStopOptions(nil))) }) } @@ -348,9 +347,7 @@ func TestToInternalProjectInfo(t *testing.T) { } t.Run("test_convert_projet_info", func(t *testing.T) { - protoProjectInfo := ToProtoProjectInfo(projectInfo) - internalProjectInfo := ToInternalProjectInfo(protoProjectInfo) - testutil.AssertEqual(t, projectInfo, internalProjectInfo) + testutil.AssertEqual(t, projectInfo, ToInternalProjectInfo(ToProtoProjectInfo(projectInfo))) }) } @@ -369,8 +366,10 @@ func TestToInternalUpdateOpts(t *testing.T) { } t.Run("test_convert_update_options", func(t *testing.T) { - protoUpdateOpts := ToProtoUpdateOptions(updateOpts) - internalUpdateOpts := ToInternalUpdateOptions(protoUpdateOpts) - testutil.AssertEqual(t, updateOpts, internalUpdateOpts) + testutil.AssertEqual(t, updateOpts, ToInternalUpdateOptions(ToProtoUpdateOptions(updateOpts))) + }) + + t.Run("test_convert_update_options_nil", func(t *testing.T) { + testutil.AssertEqual(t, &internaltypes.UpdateOpts{RestartPolicy: nil, Resources: nil}, ToInternalUpdateOptions(ToProtoUpdateOptions(nil))) }) } diff --git a/containerm/util/protobuf/convert_to_internal_util.go b/containerm/util/protobuf/convert_to_internal_util.go index 34b5da1..0f71f7e 100644 --- a/containerm/util/protobuf/convert_to_internal_util.go +++ b/containerm/util/protobuf/convert_to_internal_util.go @@ -360,7 +360,7 @@ func ToInternalLogModeConfig(grpcLogModeConfig *apitypescontainers.LogModeConfig // ToInternalStopOptions converts a types.StopOptions to an internal StopOptions func ToInternalStopOptions(grpcStopOptions *apitypescontainers.StopOptions) *internaltypes.StopOpts { if grpcStopOptions == nil { - return &internaltypes.StopOpts{} + return nil } return &internaltypes.StopOpts{ Timeout: grpcStopOptions.Timeout, diff --git a/containerm/util/protobuf/convert_to_proto_util.go b/containerm/util/protobuf/convert_to_proto_util.go index d97f525..595dbca 100644 --- a/containerm/util/protobuf/convert_to_proto_util.go +++ b/containerm/util/protobuf/convert_to_proto_util.go @@ -327,6 +327,9 @@ func ToProtoLogModeConfig(internalLogModeConfig *internaltypes.LogModeConfigurat // ToProtoStopOptions converts an internal StopOpts instance to a types.StopOpts one func ToProtoStopOptions(intenralStopOpts *internaltypes.StopOpts) *apitypescontainers.StopOptions { + if intenralStopOpts == nil { + return nil + } return &apitypescontainers.StopOptions{ Timeout: intenralStopOpts.Timeout, Force: intenralStopOpts.Force, @@ -336,6 +339,9 @@ func ToProtoStopOptions(intenralStopOpts *internaltypes.StopOpts) *apitypesconta // ToProtoUpdateOptions converts an internal UpdateOpts instance to a types.UpdateOpts one func ToProtoUpdateOptions(intenralUpdateOpts *internaltypes.UpdateOpts) *apitypescontainers.UpdateOptions { + if intenralUpdateOpts == nil { + return nil + } return &apitypescontainers.UpdateOptions{ RestartPolicy: ToProtoRestartPolicy(intenralUpdateOpts.RestartPolicy), Resources: ToProtoResource(intenralUpdateOpts.Resources), diff --git a/go.mod b/go.mod index cc0099e..0cc2072 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,8 @@ require ( github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.3 github.com/google/uuid v1.3.1 + github.com/notaryproject/notation-core-go v1.0.1 + github.com/notaryproject/notation-go v1.0.1 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc6 github.com/opencontainers/runc v1.1.12 @@ -33,16 +35,18 @@ require ( github.com/vishvananda/netlink v1.2.1-beta.2 golang.org/x/crypto v0.18.0 golang.org/x/net v0.18.0 - golang.org/x/sync v0.3.0 + golang.org/x/sync v0.4.0 golang.org/x/sys v0.16.0 google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.0 + oras.land/oras-go/v2 v2.3.1 ) require ( + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/hcsshim v0.11.4 // indirect github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect @@ -58,10 +62,14 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/libkv v0.2.1 // indirect + github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-ldap/ldap/v3 v3.4.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/googleapis v1.4.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -74,7 +82,7 @@ require ( github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/memberlist v0.3.1 // indirect github.com/hashicorp/serf v0.9.7 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062 // indirect github.com/klauspost/compress v1.16.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect @@ -93,13 +101,15 @@ require ( github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/numcpus v0.4.0 // indirect + github.com/veraison/go-cose v1.1.0 // indirect github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/mod v0.11.0 // indirect + golang.org/x/mod v0.13.0 // indirect golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.10.0 // indirect + golang.org/x/tools v0.13.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect ) diff --git a/go.sum b/go.sum index b5352ab..f46363b 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -44,6 +46,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= @@ -117,12 +121,18 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= +github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= +github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -133,6 +143,8 @@ github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4Oe github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -252,8 +264,9 @@ github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062 h1:G1+wBT0dwjIrBdLy0MIG0i+E4CQxEnedHXdauJEIH6g= github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -313,6 +326,10 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/notaryproject/notation-core-go v1.0.1 h1:01doxjDERbd0vocLQrlJdusKrRLNNn50OJzp0c5I4Cw= +github.com/notaryproject/notation-core-go v1.0.1/go.mod h1:rayl8WlKgS4YxOZgDO0iGGB4Ef515ZFZUFaZDmsPXgE= +github.com/notaryproject/notation-go v1.0.1 h1:D3fqG3eaBKVESRySV/Tg//MyTg2Q1nTKPh/t2q9LpSw= +github.com/notaryproject/notation-go v1.0.1/go.mod h1:VonyZsbocRQQNIDq/VPV5jKJOQwDH3gvfK4cXNpUA0U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU= @@ -387,6 +404,8 @@ github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03O github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= +github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o= +github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= @@ -394,11 +413,14 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17 github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= @@ -427,6 +449,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -464,8 +488,10 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -506,6 +532,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -531,8 +560,10 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -588,15 +619,24 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -607,6 +647,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -668,8 +712,10 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -809,6 +855,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +oras.land/oras-go/v2 v2.3.1 h1:lUC6q8RkeRReANEERLfH86iwGn55lbSWP20egdFHVec= +oras.land/oras-go/v2 v2.3.1/go.mod h1:5AQXVEu1X/FKp1F9DMOb5ZItZBOa0y5dha0yCm4NR9c= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/integration/ctr_management_cli_test.go b/integration/ctr_management_cli_test.go index dac4f81..59477ff 100644 --- a/integration/ctr_management_cli_test.go +++ b/integration/ctr_management_cli_test.go @@ -40,7 +40,8 @@ import ( var testdataFS embed.FS type cliTestConfiguration struct { - KantoHost string `env:"KANTO_HOST" envDefault:"/run/container-management/container-management.sock"` + KantoHost string `env:"KANTO_HOST" envDefault:"/run/container-management/container-management.sock"` + ContainerConfig string `env:"CONTAINER_CONFIG" envDefault:"./testdata/container.json"` } func init() { @@ -51,6 +52,7 @@ func TestCtrMgrCLI(t *testing.T) { cliTestConfiguration := &cliTestConfiguration{} require.NoError(t, env.Parse(cliTestConfiguration, env.Options{RequiredIfNoDef: true})) require.NoError(t, os.Setenv("KANTO_HOST", cliTestConfiguration.KantoHost)) + require.NoError(t, os.Setenv("CONTAINER_CONFIG", cliTestConfiguration.ContainerConfig)) if exist, _ := util.IsDirectory(TestData); !exist { require.NoError(t, dumpTestdata()) diff --git a/integration/framework/cli/cmd_base.go b/integration/framework/cli/cmd_base.go index 4e27b52..ed103ad 100644 --- a/integration/framework/cli/cmd_base.go +++ b/integration/framework/cli/cmd_base.go @@ -105,7 +105,8 @@ func RunCmdTestCases(t *testing.T, cmdList []TestCaseCMD) { if cmd.setupCmd != nil { runMultipleCommands(t, *cmd.setupCmd) } - result := icmd.RunCmd(cmd.icmd) + checkArguments(t, &cmd.icmd) + result := icmd.RunCommand(cmd.icmd.Command[0], cmd.icmd.Command[1:]...) if cmd.goldenFile != "" { assert.Assert(t, golden.String(result.Stdout(), cmd.goldenFile)) } @@ -122,25 +123,36 @@ func RunCmdTestCases(t *testing.T, cmdList []TestCaseCMD) { } } -func runMultipleCommands(t *testing.T, cmdArr []icmd.Cmd) { - for _, cmd := range cmdArr { - result := icmd.RunCmd(cmd) - result.Assert(t, icmd.Expected{ExitCode: 0}) - } -} - -func buildCmd(binary string, args ...string) *icmd.Cmd { - envArgs := []string{} - for _, arg := range args { +func checkArguments(t *testing.T, cmd *icmd.Cmd) { + execCmd := []string{} + for _, arg := range cmd.Command { + if strings.HasPrefix(arg, "$(") && strings.HasSuffix(arg, ")") { + arg = strings.TrimPrefix(arg, "$(") + arg = strings.TrimSuffix(arg, ")") + arguments := strings.Split(arg, " ") + cmd := icmd.Command(arguments[0], arguments[1:]...) + checkArguments(t, &cmd) + result := icmd.RunCmd(cmd) + assert.Equal(t, result.ExitCode, 0) + execCmd = append(execCmd, strings.Split(strings.TrimSuffix(strings.TrimSuffix(result.Stdout(), "\n"), " "), " ")...) + continue + } if strings.HasPrefix(arg, "$") { if val, ok := os.LookupEnv(strings.TrimPrefix(arg, "$")); ok { arg = val } } - envArgs = append(envArgs, arg) + execCmd = append(execCmd, arg) + } + *cmd = icmd.Cmd{Command: execCmd} +} + +func runMultipleCommands(t *testing.T, cmdArr []icmd.Cmd) { + for _, cmd := range cmdArr { + checkArguments(t, &cmd) + result := icmd.RunCommand(cmd.Command[0], cmd.Command[1:]...) + result.Assert(t, icmd.Expected{ExitCode: 0}) } - cmd := icmd.Command(binary, envArgs...) - return &cmd } func assertCustomResult(t *testing.T, result icmd.Result, name string, args ...string) { @@ -152,7 +164,7 @@ func assertCustomResult(t *testing.T, result icmd.Result, name string, args ...s func fromAPITestCommand(cmd TestCommand) TestCaseCMD { return TestCaseCMD{ name: cmd.Name, - icmd: *buildCmd(cmd.Command.Binary, cmd.Command.Args...), + icmd: icmd.Command(cmd.Command.Binary, cmd.Command.Args...), expected: icmd.Expected{ ExitCode: cmd.Expected.ExitCode, Timeout: cmd.Expected.Timeout, @@ -174,7 +186,7 @@ func buildCmdArrFromCommand(cmd *[]Command) *[]icmd.Cmd { } cmds := make([]icmd.Cmd, 0) for _, cmd := range *cmd { - cmds = append(cmds, *buildCmd(cmd.Binary, cmd.Args...)) + cmds = append(cmds, icmd.Command(cmd.Binary, cmd.Args...)) } return &cmds } diff --git a/integration/testdata/container.json b/integration/testdata/container.json new file mode 100644 index 0000000..5d7a656 --- /dev/null +++ b/integration/testdata/container.json @@ -0,0 +1,6 @@ +{ + "container_name": "create_container_from_file", + "image": { + "name": "docker.io/library/influxdb:1.8.4" + } +} diff --git a/integration/testdata/create-help.golden b/integration/testdata/create-help.golden index 1267beb..e2e2a93 100644 --- a/integration/testdata/create-help.golden +++ b/integration/testdata/create-help.golden @@ -1,7 +1,7 @@ Create a container. Usage: - kanto-cm create [option]... container-image-id [command] [command-arg]... + kanto-cm create [option]... [container-image-id] [command] [command-arg]... Examples: create container-image-id @@ -16,6 +16,7 @@ Flags: --e=VAR1=2 --e=VAR2="a bc" If --e=VAR1= is used, the environment variable would be set to empty. If --e=VAR1 is used, the environment variable would be removed from the container environment inherited from the image. + -f, --file string Creates a container with a predefined config given by the user. -h, --help help for create --hosts strings Extra hosts to be added in the current container's /etc/hosts file. Example: --hosts="hostname1:, hostname2:.." diff --git a/integration/testdata/create-test.yaml b/integration/testdata/create-test.yaml index 5d4fa06..8840a65 100644 --- a/integration/testdata/create-test.yaml +++ b/integration/testdata/create-test.yaml @@ -12,7 +12,7 @@ command: args: ["create", "--host", "$KANTO_HOST"] expected: exitCode: 1 - err: "Error: requires at least 1 arg(s), only received 0" + err: "Error: container image argument is expected" --- name: create_influxdb_container command: @@ -33,4 +33,25 @@ command: args: ["create", "--host", "$KANTO_HOST", "invalid"] expected: exitCode: 1 - err: "Error: rpc error: code = Unknown desc = failed to resolve reference \"invalid\": object required" \ No newline at end of file + err: "Error: rpc error: code = Unknown desc = failed to resolve reference \"invalid\": object required" +--- +name: create_container_from_file +command: + binary: kanto-cm + args: ["create", "--file", "$CONTAINER_CONFIG", "--host", "$KANTO_HOST"] +expected: + exitCode: 0 +customResult: + type: REGEX + args: ["([A-Za-z0-9]+(-[A-Za-z0-9]+)+)"] +onExit: + - binary: "kanto-cm" + args: ["remove", "--host", "$KANTO_HOST", "-n", "create_container_from_file", "-f"] +--- +name: create_container_from_file_with_flags +command: + binary: kanto-cm + args: ["create", "--file", "$CONTAINER_CONFIG", "--privileged", "true", "--host", "$KANTO_HOST"] +expected: + exitCode: 1 + err: "Error: no arguments are expected when creating a container from file" \ No newline at end of file diff --git a/integration/testdata/list-help.golden b/integration/testdata/list-help.golden index 32f3de7..c05fbf1 100644 --- a/integration/testdata/list-help.golden +++ b/integration/testdata/list-help.golden @@ -4,13 +4,16 @@ Usage: kanto-cm list Examples: -list + list list --name - list -n + list --quiet + list --filter status=created Flags: - -h, --help help for list - -n, --name string List all containers with a specific name. + --filter strings Lists only containers with a specified filter. The containers can be filtered by their status, image and exitcode. + -h, --help help for list + -n, --name string List all containers with a specific name. + -q, --quiet List only container IDs. Global Flags: --debug Switch commands log level to DEBUG mode diff --git a/integration/testdata/list-test.yaml b/integration/testdata/list-test.yaml index fc3bf9f..f809fcd 100644 --- a/integration/testdata/list-test.yaml +++ b/integration/testdata/list-test.yaml @@ -12,7 +12,7 @@ command: args: ["list", "--host", "$KANTO_HOST"] expected: exitCode: 0 - out: "No found containers." + out: "No containers found." --- name: list_all_containers setupCmd: @@ -44,11 +44,46 @@ onExit: - binary: "kanto-cm" args: ["stop", "--host", "$KANTO_HOST", "-s", "SIGKILL", "-n", "list_containers_with_state_running"] - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_with_state_running", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] +--- +name: list_containers_with_quiet +setupCmd: + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_quiet_one", "docker.io/library/influxdb:1.8.4"] + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_quiet_two", "docker.io/library/influxdb:1.8.4"] +command: + binary: kanto-cm + args: ["list", "--host", "$KANTO_HOST", "--quiet"] +expected: + exitCode: 0 +customResult: + type: REGEX + args: ["[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"] +onExit: - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_with_state_stopped", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] +--- +name: list_containers_with_filter +setupCmd: + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_filter_one", "docker.io/library/influxdb:1.8.4"] + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_filter_two", "docker.io/library/influxdb:1.8.4"] +command: + binary: kanto-cm + args: ["list", "--host", "$KANTO_HOST", "--filter", "status=created"] +expected: + exitCode: 0 +customResult: + type: REGEX + args: ["ID |Name |Image |Status |Finished At |Exit Code | + ------------------------------------- |------------------------------------- |------------------------------------------------------------ |---------- |------------------------------ |---------- | + [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12} |list_containers_with_filter_one |docker.io/library/influxdb:1.8.4 |Created | |0 | + [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12} |list_containers_with_filter_two |docker.io/library/influxdb:1.8.4 |Created | |0 |"] +onExit: - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_with_state_created", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] --- name: list_existing_container setupCmd: @@ -69,9 +104,7 @@ customResult: ] onExit: - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_ctr0", "-f"] - - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_ctr1", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] --- name: list_not_existing_container command: @@ -79,7 +112,7 @@ command: args: ["list", "--host", "$KANTO_HOST", "-n", "invalid"] expected: exitCode: 0 - out: "No found containers." + out: "No containers found." --- name: list_invalid_arg command: diff --git a/integration/testdata/remove-help.golden b/integration/testdata/remove-help.golden index da894a5..e28dc33 100644 --- a/integration/testdata/remove-help.golden +++ b/integration/testdata/remove-help.golden @@ -1,10 +1,11 @@ -Remove a container and frees the associated resources. +Remove one or more containers and frees the associated resources. Usage: - kanto-cm remove + kanto-cm remove ... Examples: -remove + remove + remove remove --name remove -n diff --git a/integration/testdata/remove-test.yaml b/integration/testdata/remove-test.yaml index dfec002..3de6ac9 100644 --- a/integration/testdata/remove-test.yaml +++ b/integration/testdata/remove-test.yaml @@ -58,4 +58,16 @@ onExit: - binary: kanto-cm args: ["stop", "--host", "$KANTO_HOST", "-s", "SIGKILL", "-n", "remove_container_with_state_running"] - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "remove_container_with_state_running", "-f"] \ No newline at end of file + args: ["remove", "--host", "$KANTO_HOST", "-n", "remove_container_with_state_running", "-f"] +--- +name: remove_multiple_containers +setupCmd: + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "remove_container_one", "docker.io/library/influxdb:1.8.4"] + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "remove_container_two", "docker.io/library/influxdb:1.8.4"] +command: + binary: kanto-cm + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] +expected: + exitCode: 0 \ No newline at end of file diff --git a/integration/testdata/stop-help.golden b/integration/testdata/stop-help.golden index c423665..48f0ec9 100644 --- a/integration/testdata/stop-help.golden +++ b/integration/testdata/stop-help.golden @@ -4,7 +4,7 @@ Usage: kanto-cm stop Examples: -stop + stop stop --name stop -n @@ -13,6 +13,7 @@ Flags: -h, --help help for stop -n, --name string Stop a container with a specific name. -s, --signal string Stop a container using a specific signal. Signals could be specified by using their names or numbers, e.g. SIGINT or 2. (default "SIGTERM") + -t, --time int Sets the timeout period in seconds to gracefully stop the container. When timeout expires the container process would be forcibly killed. (default -9223372036854775808) Global Flags: --debug Switch commands log level to DEBUG mode diff --git a/integration/testdata/stop-test.yaml b/integration/testdata/stop-test.yaml index 1d7ac45..dac4350 100644 --- a/integration/testdata/stop-test.yaml +++ b/integration/testdata/stop-test.yaml @@ -1,4 +1,4 @@ -name: remove_help +name: stop_help command: binary: kanto-cm args: ["stop", "-h"]