diff --git a/examples/alpine/alpine.pb.txt b/examples/alpine/alpine.pb.txt new file mode 100644 index 00000000..ddde2bbf --- /dev/null +++ b/examples/alpine/alpine.pb.txt @@ -0,0 +1,49 @@ +name: "twodut-alpine" +nodes: { + name: "alpine" + vendor: ALPINE + config: { + image: "sonic-vs:latest" + vendor_data { + [type.googleapis.com/alpine.AlpineConfig] { + containers: { + name: "dataplane" + image: "us-west1-docker.pkg.dev/openconfig-lemming/release/lucius:ga" + command: "/lucius/lucius" + args: "-alsologtostderr" + } + } + } + } + services:{ + key: 22 + value: { + name: "ssh" + inside: 22 + } + } +} +nodes: { + name: "host" + vendor: HOST + services:{ + key: 22 + value: { + name: "ssh" + inside: 22 + } + } +} + +links: { + a_node: "alpine" + a_int: "eth1" + z_node: "host" + z_int: "eth1" +} +links: { + a_node: "alpine" + a_int: "eth2" + z_node: "host" + z_int: "eth2" +} \ No newline at end of file diff --git a/proto/alpine.proto b/proto/alpine.proto new file mode 100644 index 00000000..aaeadb9f --- /dev/null +++ b/proto/alpine.proto @@ -0,0 +1,30 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; + +package alpine; + +option go_package = "github.com/openconfig/kne/proto/alpine"; + +// Alpine specific vendor data for KNE +message AlpineConfig { + repeated Container containers = 1; +} + +message Container { + string name = 1; + string image = 2; + repeated string command = 3; + repeated string args = 4; +} diff --git a/proto/alpine/alpine.pb.go b/proto/alpine/alpine.pb.go new file mode 100644 index 00000000..f8163532 --- /dev/null +++ b/proto/alpine/alpine.pb.go @@ -0,0 +1,251 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v3.21.12 +// source: alpine.proto + +package alpine + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Alpine specific vendor data for KNE +type AlpineConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` +} + +func (x *AlpineConfig) Reset() { + *x = AlpineConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_alpine_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AlpineConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlpineConfig) ProtoMessage() {} + +func (x *AlpineConfig) ProtoReflect() protoreflect.Message { + mi := &file_alpine_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AlpineConfig.ProtoReflect.Descriptor instead. +func (*AlpineConfig) Descriptor() ([]byte, []int) { + return file_alpine_proto_rawDescGZIP(), []int{0} +} + +func (x *AlpineConfig) GetContainers() []*Container { + if x != nil { + return x.Containers + } + return nil +} + +type Container struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` + Command []string `protobuf:"bytes,3,rep,name=command,proto3" json:"command,omitempty"` + Args []string `protobuf:"bytes,4,rep,name=args,proto3" json:"args,omitempty"` +} + +func (x *Container) Reset() { + *x = Container{} + if protoimpl.UnsafeEnabled { + mi := &file_alpine_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Container) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Container) ProtoMessage() {} + +func (x *Container) ProtoReflect() protoreflect.Message { + mi := &file_alpine_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Container.ProtoReflect.Descriptor instead. +func (*Container) Descriptor() ([]byte, []int) { + return file_alpine_proto_rawDescGZIP(), []int{1} +} + +func (x *Container) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Container) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +func (x *Container) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *Container) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +var File_alpine_proto protoreflect.FileDescriptor + +var file_alpine_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x61, 0x6c, 0x70, 0x69, 0x6e, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x61, 0x6c, 0x70, 0x69, 0x6e, 0x65, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x6c, 0x70, 0x69, 0x6e, 0x65, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x6c, 0x70, + 0x69, 0x6e, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x63, 0x0a, 0x09, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, + 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x42, 0x28, + 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, + 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x6b, 0x6e, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x61, 0x6c, 0x70, 0x69, 0x6e, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_alpine_proto_rawDescOnce sync.Once + file_alpine_proto_rawDescData = file_alpine_proto_rawDesc +) + +func file_alpine_proto_rawDescGZIP() []byte { + file_alpine_proto_rawDescOnce.Do(func() { + file_alpine_proto_rawDescData = protoimpl.X.CompressGZIP(file_alpine_proto_rawDescData) + }) + return file_alpine_proto_rawDescData +} + +var file_alpine_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_alpine_proto_goTypes = []interface{}{ + (*AlpineConfig)(nil), // 0: alpine.AlpineConfig + (*Container)(nil), // 1: alpine.Container +} +var file_alpine_proto_depIdxs = []int32{ + 1, // 0: alpine.AlpineConfig.containers:type_name -> alpine.Container + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_alpine_proto_init() } +func file_alpine_proto_init() { + if File_alpine_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_alpine_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AlpineConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_alpine_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Container); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_alpine_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_alpine_proto_goTypes, + DependencyIndexes: file_alpine_proto_depIdxs, + MessageInfos: file_alpine_proto_msgTypes, + }.Build() + File_alpine_proto = out.File + file_alpine_proto_rawDesc = nil + file_alpine_proto_goTypes = nil + file_alpine_proto_depIdxs = nil +} diff --git a/proto/generate.go b/proto/generate.go index bbe7c805..39b168e0 100644 --- a/proto/generate.go +++ b/proto/generate.go @@ -1,5 +1,6 @@ package proto +//go:generate protoc --go_out=./alpine --go_opt=paths=source_relative ./alpine.proto //go:generate protoc --go_out=./topo --go_opt=paths=source_relative ./topo.proto //go:generate protoc --go_out=./ceos --go_opt=paths=source_relative ./ceos.proto //go:generate protoc --go_out=./controller --go-grpc_out=./controller --go-grpc_opt=paths=source_relative --go_opt=paths=source_relative ./controller.proto diff --git a/proto/topo.proto b/proto/topo.proto index c4fe6b5a..659d244e 100644 --- a/proto/topo.proto +++ b/proto/topo.proto @@ -41,6 +41,7 @@ enum Vendor { GOBGP = 8; NOKIA = 9; OPENCONFIG = 10; + ALPINE = 11; } // Node is a single container inside the topology diff --git a/proto/topo/topo.pb.go b/proto/topo/topo.pb.go index fcd4a82e..696fef6c 100644 --- a/proto/topo/topo.pb.go +++ b/proto/topo/topo.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.31.0 // protoc v3.21.12 // source: topo.proto @@ -51,6 +51,7 @@ const ( Vendor_GOBGP Vendor = 8 Vendor_NOKIA Vendor = 9 Vendor_OPENCONFIG Vendor = 10 + Vendor_ALPINE Vendor = 11 ) // Enum value maps for Vendor. @@ -67,6 +68,7 @@ var ( 8: "GOBGP", 9: "NOKIA", 10: "OPENCONFIG", + 11: "ALPINE", } Vendor_value = map[string]int32{ "UNKNOWN": 0, @@ -80,6 +82,7 @@ var ( "GOBGP": 8, "NOKIA": 9, "OPENCONFIG": 10, + "ALPINE": 11, } ) @@ -267,7 +270,7 @@ type Node struct { unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Name of the node in the topology. Must be unique. - // Deprecated: Do not use. + // Deprecated: Marked as deprecated in topo.proto. Type Node_Type `protobuf:"varint,2,opt,name=type,proto3,enum=topo.Node_Type" json:"type,omitempty"` Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Metadata labels describing the node. Config *Config `protobuf:"bytes,5,opt,name=config,proto3" json:"config,omitempty"` // Pod specific configuration of the node. @@ -329,7 +332,7 @@ func (x *Node) GetName() string { return "" } -// Deprecated: Do not use. +// Deprecated: Marked as deprecated in topo.proto. func (x *Node) GetType() Node_Type { if x != nil { return x.Type @@ -1371,7 +1374,7 @@ var file_topo_proto_rawDesc = []byte{ 0x64, 0x65, 0x49, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x49, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x8c, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x2a, 0x98, 0x01, 0x0a, 0x06, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x52, 0x49, 0x53, 0x54, 0x41, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, @@ -1380,10 +1383,11 @@ var file_topo_proto_rawDesc = []byte{ 0x10, 0x05, 0x12, 0x07, 0x0a, 0x03, 0x46, 0x52, 0x52, 0x10, 0x06, 0x12, 0x0a, 0x0a, 0x06, 0x51, 0x55, 0x41, 0x47, 0x47, 0x41, 0x10, 0x07, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x4f, 0x42, 0x47, 0x50, 0x10, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x4e, 0x4f, 0x4b, 0x49, 0x41, 0x10, 0x09, 0x12, 0x0e, 0x0a, - 0x0a, 0x4f, 0x50, 0x45, 0x4e, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x0a, 0x42, 0x26, 0x5a, - 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x6b, 0x6e, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2f, 0x74, 0x6f, 0x70, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x0a, 0x4f, 0x50, 0x45, 0x4e, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x0a, 0x12, 0x0a, 0x0a, + 0x06, 0x41, 0x4c, 0x50, 0x49, 0x4e, 0x45, 0x10, 0x0b, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2f, 0x6b, 0x6e, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x6f, 0x70, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/topo/node/alpine/alpine.go b/topo/node/alpine/alpine.go new file mode 100644 index 00000000..08058e30 --- /dev/null +++ b/topo/node/alpine/alpine.go @@ -0,0 +1,198 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package alpine + +import ( + "context" + "fmt" + + apb "github.com/openconfig/kne/proto/alpine" + tpb "github.com/openconfig/kne/proto/topo" + "github.com/openconfig/kne/topo/node" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + log "k8s.io/klog/v2" + "k8s.io/utils/pointer" +) + +func New(nodeImpl *node.Impl) (node.Node, error) { + if nodeImpl == nil { + return nil, fmt.Errorf("nodeImpl cannot be nil") + } + if nodeImpl.Proto == nil { + return nil, fmt.Errorf("nodeImpl.Proto cannot be nil") + } + cfg := defaults(nodeImpl.Proto) + nodeImpl.Proto = cfg + n := &Node{ + Impl: nodeImpl, + } + return n, nil +} + +type Node struct { + *node.Impl +} + +func (n *Node) Create(ctx context.Context) error { + if err := n.ValidateConstraints(); err != nil { + return fmt.Errorf("node %s failed to validate node with errors: %s", n.Name(), err) + } + if err := n.CreatePod(ctx); err != nil { + return fmt.Errorf("node %s failed to create pod %w", n.Name(), err) + } + if err := n.CreateService(ctx); err != nil { + return fmt.Errorf("node %s failed to create service %w", n.Name(), err) + } + return nil +} + +// CreatePod creates a Pod for the Node based on the underlying proto. +func (n *Node) CreatePod(ctx context.Context) error { + pb := n.Proto + log.Infof("Creating Pod:\n %+v", pb) + + initContainerImage := pb.Config.InitImage + if initContainerImage == "" { + initContainerImage = node.DefaultInitContainerImage + } + + alpineContainers := []corev1.Container{ + { + Name: pb.Name, + Image: pb.Config.Image, + Command: pb.Config.Command, + Args: pb.Config.Args, + Env: node.ToEnvVar(pb.Config.Env), + // TODO: Update resources to the containers as per the constraints + Resources: node.ToResourceRequirements(pb.Constraints), + ImagePullPolicy: "IfNotPresent", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + } + + if vendorData := pb.Config.GetVendorData(); vendorData != nil { + alpineConfig := &apb.AlpineConfig{} + + if err := vendorData.UnmarshalTo(alpineConfig); err != nil { + return err + } + + switch len := len(alpineConfig.Containers); len { + case 0: + log.Infof("Alpine custom containers not found.") + case 1: + dpContainer := alpineConfig.Containers[0] + + alpineContainers = append(alpineContainers, + corev1.Container{ + Name: dpContainer.Name, + Image: dpContainer.Image, + Command: dpContainer.Command, + Args: dpContainer.Args, + Env: node.ToEnvVar(pb.Config.Env), + // TODO: Update resources to the containers as per the constraints + Resources: node.ToResourceRequirements(pb.Constraints), + ImagePullPolicy: "IfNotPresent", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + ) + default: + // Only Dataplane container is supported as the custom container + return fmt.Errorf("Alpine supports only 1 custom container, %d provided.", len) + } + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: pb.Name, + Labels: map[string]string{ + "app": pb.Name, + "topo": n.Namespace, + }, + }, + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: fmt.Sprintf("init-%s", pb.Name), + Image: initContainerImage, + Args: []string{ + fmt.Sprintf("%d", len(n.Proto.Interfaces)+1), + fmt.Sprintf("%d", pb.Config.Sleep), + }, + ImagePullPolicy: "IfNotPresent", + }}, + Containers: alpineContainers, + TerminationGracePeriodSeconds: pointer.Int64(0), + NodeSelector: map[string]string{}, + Affinity: &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{{ + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: "topo", + Operator: "In", + Values: []string{pb.Name}, + }}, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }}, + }, + }, + }, + } + sPod, err := n.KubeClient.CoreV1().Pods(n.Namespace).Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + return err + } + log.Infof("Pod created:\n%+v\n", sPod) + + return nil +} + +func defaults(pb *tpb.Node) *tpb.Node { + if pb.Config == nil { + pb.Config = &tpb.Config{} + } + if len(pb.GetConfig().GetCommand()) == 0 { + pb.Config.Command = []string{"go", "run", "main.go"} + } + if pb.Config.EntryCommand == "" { + pb.Config.EntryCommand = fmt.Sprintf("kubectl exec -it %s -- sh", pb.Name) + } + if pb.Config.Image == "" { + pb.Config.Image = "alpine:latest" + } + if pb.Services == nil { + pb.Services = map[uint32]*tpb.Service{ + 22: { + Name: "ssh", + Inside: 22, + }, + } + } + // TODO: Add appropriate default constraints for the Alpine KNE node + + return pb +} + +func init() { + node.Vendor(tpb.Vendor_ALPINE, New) +} diff --git a/topo/node/alpine/alpine_test.go b/topo/node/alpine/alpine_test.go new file mode 100644 index 00000000..ff9b4c7e --- /dev/null +++ b/topo/node/alpine/alpine_test.go @@ -0,0 +1,223 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package alpine + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/h-fam/errdiff" + apb "github.com/openconfig/kne/proto/alpine" + tpb "github.com/openconfig/kne/proto/topo" + "github.com/openconfig/kne/topo/node" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kfake "k8s.io/client-go/kubernetes/fake" + "k8s.io/utils/pointer" +) + +func TestNew(t *testing.T) { + tests := []struct { + desc string + nImpl *node.Impl + want *tpb.Node + wantErr string + }{{ + desc: "nil impl", + wantErr: "nodeImpl cannot be nil", + }, { + desc: "nil pb", + wantErr: "nodeImpl.Proto cannot be nil", + nImpl: &node.Impl{}, + }, { + desc: "empty pb", + nImpl: &node.Impl{ + Proto: &tpb.Node{}, + }, + want: &tpb.Node{ + Config: &tpb.Config{ + Command: []string{"go", "run", "main.go"}, + EntryCommand: fmt.Sprintf("kubectl exec -it %s -- sh", ""), + Image: "alpine:latest", + }, + Services: map[uint32]*tpb.Service{ + 22: { + Name: "ssh", + Inside: 22, + }, + }, + }, + }, { + desc: "provided alpine container", + nImpl: &node.Impl{ + Proto: &tpb.Node{ + Config: &tpb.Config{ + Image: "alpine:latest", + Command: []string{"go", "run", "main.go"}, + }, + }, + }, + want: &tpb.Node{ + Config: &tpb.Config{ + Image: "alpine:latest", + Command: []string{"go", "run", "main.go"}, + EntryCommand: fmt.Sprintf("kubectl exec -it %s -- sh", ""), + }, + Services: map[uint32]*tpb.Service{ + 22: { + Name: "ssh", + Inside: 22, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + n, err := New(tt.nImpl) + if s := errdiff.Substring(err, tt.wantErr); s != "" { + t.Fatalf("unexpected error: got %v, want %s", err, s) + } + if tt.wantErr != "" { + return + } + if !proto.Equal(n.GetProto(), tt.want) { + t.Fatalf("New() failed: got\n%swant\n%s", prototext.Format(n.GetProto()), prototext.Format(tt.want)) + } + }) + } +} + +func TestCreateNode(t *testing.T) { + vendorData, err := anypb.New(&apb.AlpineConfig{ + Containers: []*apb.Container{ + { + Name: "dp", + Image: "dpImage", + Command: []string{"dpCommand"}, + Args: []string{"dpArgs"}, + }, + }, + }) + if err != nil { + t.Fatalf("cannot marshal AlpineConfig into \"any\" protobuf: %v", err) + } + tests := []struct { + desc string + nImpl *node.Impl + wantAlpineCtr corev1.Container + wantDpCtr corev1.Container + wantErr string + }{{ + desc: "get all containers", + nImpl: &node.Impl{ + Proto: &tpb.Node{ + Name: "alpine", + Config: &tpb.Config{ + Image: "alpineImage", + Command: []string{"alpineCommand"}, + Args: []string{"alpineArgs"}, + VendorData: vendorData, + }, + }, + }, + wantAlpineCtr: corev1.Container{ + Name: "alpine", + Image: "alpineImage", + Command: []string{"alpineCommand"}, + Args: []string{"alpineArgs"}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + wantDpCtr: corev1.Container{ + Name: "dp", + Image: "dpImage", + Command: []string{"dpCommand"}, + Args: []string{"dpArgs"}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }, + { + desc: "get only alpine containers", + nImpl: &node.Impl{ + Proto: &tpb.Node{ + Name: "alpine", + Config: &tpb.Config{ + Image: "alpineImage", + Command: []string{"alpineCommand"}, + Args: []string{"alpineArgs"}, + }, + }, + }, + wantAlpineCtr: corev1.Container{ + Name: "alpine", + Image: "alpineImage", + Command: []string{"alpineCommand"}, + Args: []string{"alpineArgs"}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + }, + }} + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + n := &Node{ + Impl: &node.Impl{ + Namespace: "test", + KubeClient: kfake.NewSimpleClientset(), + Proto: tt.nImpl.Proto, + }, + } + err := n.CreatePod(context.Background()) + if s := errdiff.Substring(err, tt.wantErr); s != "" { + t.Fatalf("unexpected error: got %v, want %s", err, s) + } + pod, err := n.KubeClient.CoreV1().Pods(n.Namespace).Get(context.Background(), n.Name(), metav1.GetOptions{}) + if err != nil { + t.Fatalf("Could not get the pod: %v", err) + } + containers := pod.Spec.Containers + if len(containers) < 1 || len(containers) > 2 { + t.Fatalf("Num containers mismatch: want: 1 or 2 got:%v", len(containers)) + } + alpineCtr := containers[0] + if s := cmp.Diff(tt.wantAlpineCtr, alpineCtr); s != "" { + t.Fatalf("Alpine Container mismatch: %s,\n got:\n%v \n want:\n%v\n", s, alpineCtr, tt.wantAlpineCtr) + } + if len(containers) == 2 { + dpCtr := containers[1] + if s := cmp.Diff(tt.wantDpCtr, dpCtr); s != "" { + t.Fatalf("DP Container mismatch: %s,\n got:\n%v \n want:\n%v\n", s, dpCtr, tt.wantDpCtr) + } + } + }) + } +} diff --git a/topo/topo.go b/topo/topo.go index 5af98825..eb47a92f 100644 --- a/topo/topo.go +++ b/topo/topo.go @@ -46,6 +46,7 @@ import ( "k8s.io/client-go/tools/clientcmd" log "k8s.io/klog/v2" + _ "github.com/openconfig/kne/topo/node/alpine" _ "github.com/openconfig/kne/topo/node/arista" _ "github.com/openconfig/kne/topo/node/cisco" _ "github.com/openconfig/kne/topo/node/gobgp"