2018-04-20 17:30:12 +00:00
// Copyright (c) 2017 Intel Corporation
//
// 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 k8sclient
import (
"encoding/json"
"fmt"
2018-05-03 20:36:49 +00:00
"regexp"
2018-05-09 10:47:37 +00:00
"strings"
2018-04-20 17:30:12 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
2018-06-14 18:15:02 +00:00
v1 "k8s.io/client-go/pkg/api/v1"
2018-04-20 17:30:12 +00:00
"k8s.io/client-go/tools/clientcmd"
2018-04-27 18:43:20 +00:00
"github.com/containernetworking/cni/libcni"
2018-04-20 17:30:12 +00:00
"github.com/containernetworking/cni/pkg/skel"
cnitypes "github.com/containernetworking/cni/pkg/types"
2018-05-01 18:08:39 +00:00
"github.com/intel/multus-cni/types"
2018-04-20 17:30:12 +00:00
)
// NoK8sNetworkError indicates error, no network in kubernetes
2018-04-23 06:51:11 +00:00
type NoK8sNetworkError struct {
message string
}
2018-04-20 17:30:12 +00:00
2018-04-23 06:51:11 +00:00
func ( e * NoK8sNetworkError ) Error ( ) string { return string ( e . message ) }
2018-04-20 17:30:12 +00:00
2018-06-14 18:15:02 +00:00
type defaultKubeClient struct {
client kubernetes . Interface
}
// defaultKubeClient implements KubeClient
var _ KubeClient = & defaultKubeClient { }
func ( d * defaultKubeClient ) GetRawWithPath ( path string ) ( [ ] byte , error ) {
return d . client . ExtensionsV1beta1 ( ) . RESTClient ( ) . Get ( ) . AbsPath ( path ) . DoRaw ( )
}
2018-04-20 17:30:12 +00:00
2018-06-14 18:15:02 +00:00
func ( d * defaultKubeClient ) GetPod ( namespace , name string ) ( * v1 . Pod , error ) {
return d . client . Core ( ) . Pods ( namespace ) . Get ( name , metav1 . GetOptions { } )
}
func createK8sClient ( kubeconfig string ) ( KubeClient , error ) {
2018-04-20 17:30:12 +00:00
// uses the current context in kubeconfig
config , err := clientcmd . BuildConfigFromFlags ( "" , kubeconfig )
if err != nil {
2018-04-24 02:29:44 +00:00
return nil , fmt . Errorf ( "createK8sClient: failed to get context for the kubeconfig %v, refer Multus README.md for the usage guide: %v" , kubeconfig , err )
2018-04-20 17:30:12 +00:00
}
// creates the clientset
2018-06-14 18:15:02 +00:00
client , err := kubernetes . NewForConfig ( config )
if err != nil {
return nil , err
}
return & defaultKubeClient { client : client } , nil
2018-04-20 17:30:12 +00:00
}
2018-06-15 03:31:22 +00:00
func getPodNetworkAnnotation ( client KubeClient , k8sArgs types . K8sArgs ) ( string , string , error ) {
2018-04-20 17:30:12 +00:00
var err error
2018-06-14 18:15:02 +00:00
pod , err := client . GetPod ( string ( k8sArgs . K8S_POD_NAMESPACE ) , string ( k8sArgs . K8S_POD_NAME ) )
2018-04-20 17:30:12 +00:00
if err != nil {
2018-06-15 03:31:22 +00:00
return "" , "" , fmt . Errorf ( "getPodNetworkAnnotation: failed to query the pod %v in out of cluster comm: %v" , string ( k8sArgs . K8S_POD_NAME ) , err )
2018-04-20 17:30:12 +00:00
}
2018-06-15 03:31:22 +00:00
return pod . Annotations [ "kubernetes.v1.cni.cncf.io/networks" ] , pod . ObjectMeta . Namespace , nil
2018-04-20 17:30:12 +00:00
}
func parsePodNetworkObjectName ( podnetwork string ) ( string , string , string , error ) {
var netNsName string
var netIfName string
var networkName string
slashItems := strings . Split ( podnetwork , "/" )
if len ( slashItems ) == 2 {
netNsName = strings . TrimSpace ( slashItems [ 0 ] )
networkName = slashItems [ 1 ]
} else if len ( slashItems ) == 1 {
networkName = slashItems [ 0 ]
} else {
return "" , "" , "" , fmt . Errorf ( "Invalid network object (failed at '/')" )
}
atItems := strings . Split ( networkName , "@" )
networkName = strings . TrimSpace ( atItems [ 0 ] )
if len ( atItems ) == 2 {
netIfName = strings . TrimSpace ( atItems [ 1 ] )
} else if len ( atItems ) != 1 {
return "" , "" , "" , fmt . Errorf ( "Invalid network object (failed at '@')" )
}
2018-05-03 20:36:49 +00:00
// Check and see if each item matches the specification for valid attachment name.
// "Valid attachment names must be comprised of units of the DNS-1123 label format"
// [a-z0-9]([-a-z0-9]*[a-z0-9])?
// And we allow at (@), and forward slash (/) (units separated by commas)
// It must start and end alphanumerically.
allItems := [ ] string { netNsName , networkName , netIfName }
for i := range allItems {
matched , _ := regexp . MatchString ( "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" , allItems [ i ] )
2018-05-09 10:47:37 +00:00
if ! matched && len ( [ ] rune ( allItems [ i ] ) ) > 0 {
return "" , "" , "" , fmt . Errorf ( fmt . Sprintf ( "Failed to parse: one or more items did not match comma-delimited format (must consist of lower case alphanumeric characters). Must start and end with an alphanumeric character), mismatch @ '%v'" , allItems [ i ] ) )
2018-05-03 20:36:49 +00:00
}
}
2018-04-20 17:30:12 +00:00
return netNsName , networkName , netIfName , nil
}
2018-06-20 02:27:42 +00:00
func parsePodNetworkAnnotation ( podNetworks , defaultNamespace string ) ( [ ] * types . NetworkSelectionElement , error ) {
var networks [ ] * types . NetworkSelectionElement
2018-04-20 17:30:12 +00:00
2018-06-20 02:27:42 +00:00
if podNetworks == "" {
return nil , fmt . Errorf ( "parsePodNetworkAnnotation: pod annotation not having \"network\" as key, refer Multus README.md for the usage guide" )
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
if strings . IndexAny ( podNetworks , "[{\"" ) >= 0 {
if err := json . Unmarshal ( [ ] byte ( podNetworks ) , & networks ) ; err != nil {
return nil , fmt . Errorf ( "parsePodNetworkAnnotation: failed to parse pod Network Attachment Selection Annotation JSON format: %v" , err )
}
} else {
// Comma-delimited list of network attachment object names
for _ , item := range strings . Split ( podNetworks , "," ) {
2018-05-03 20:36:49 +00:00
// Remove leading and trailing whitespace.
2018-06-20 02:27:42 +00:00
item = strings . TrimSpace ( item )
2018-05-03 20:36:49 +00:00
2018-04-20 17:30:12 +00:00
// Parse network name (i.e. <namespace>/<network name>@<ifname>)
2018-06-20 02:27:42 +00:00
netNsName , networkName , netIfName , err := parsePodNetworkObjectName ( item )
2018-04-20 17:30:12 +00:00
if err != nil {
2018-06-20 02:27:42 +00:00
return nil , fmt . Errorf ( "parsePodNetworkAnnotation: %v" , err )
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
networks = append ( networks , & types . NetworkSelectionElement {
Name : networkName ,
Namespace : netNsName ,
InterfaceRequest : netIfName ,
} )
2018-04-20 17:30:12 +00:00
}
}
2018-06-20 02:27:42 +00:00
for _ , net := range networks {
if net . Namespace == "" {
net . Namespace = defaultNamespace
}
}
return networks , nil
2018-04-20 17:30:12 +00:00
}
2018-06-15 03:47:01 +00:00
func getCNIConfig ( name string , ifname string , confdir string ) ( string , error ) {
2018-04-27 18:43:20 +00:00
2018-04-23 17:30:41 +00:00
// In the absence of valid keys in a Spec, the runtime (or
// meta-plugin) should load and execute a CNI .configlist
// or .config (in that order) file on-disk whose JSON
// “name” key matches this Network object’ s name.
2018-04-20 17:30:12 +00:00
2018-04-27 18:43:20 +00:00
//Todo
// support conflist for chaining mechanism
// In part, adapted from K8s pkg/kubelet/dockershim/network/cni/cni.go#getDefaultCNINetwork
2018-05-09 10:47:37 +00:00
files , err := libcni . ConfFiles ( confdir , [ ] string { ".conf" , ".json" } )
2018-04-27 18:43:20 +00:00
switch {
case err != nil :
2018-06-20 02:27:42 +00:00
return "" , fmt . Errorf ( "No networks found in %s" , confdir )
2018-04-27 18:43:20 +00:00
case len ( files ) == 0 :
2018-06-20 02:27:42 +00:00
return "" , fmt . Errorf ( "No networks found in %s" , confdir )
2018-04-27 18:43:20 +00:00
}
for _ , confFile := range files {
conf , err := libcni . ConfFromFile ( confFile )
if err != nil {
return "" , fmt . Errorf ( "Error loading CNI config file %s: %v" , confFile , err )
}
if conf . Network . Name == name {
// Ensure the config has a "type" so we know what plugin to run.
// Also catches the case where somebody put a conflist into a conf file.
if conf . Network . Type == "" {
return "" , fmt . Errorf ( "Error loading CNI config file %s: no 'type'; perhaps this is a .conflist?" , confFile )
}
2018-06-15 03:47:01 +00:00
return getConfig ( string ( conf . Bytes [ : ] ) , ifname ) , nil
2018-04-27 18:43:20 +00:00
}
}
2018-05-09 10:47:37 +00:00
return "" , fmt . Errorf ( "no network available in the name %s in cni dir %s" , name , confdir )
2018-04-23 17:30:41 +00:00
}
2018-06-15 03:47:01 +00:00
func getPlugin ( plugin string , name string , ifname string ) string {
2018-04-23 17:30:41 +00:00
tmpconfig := [ ] string { }
tmpconfig = append ( tmpconfig , fmt . Sprintf ( ` { "cniVersion": "0.3.1" , "name": "%s", "type": "%s" ` , name , plugin ) )
if ifname != "" {
tmpconfig = append ( tmpconfig , fmt . Sprintf ( ` , "ifnameRequest": "%s" ` , ifname ) )
}
tmpconfig = append ( tmpconfig , "}" )
2018-04-27 18:43:20 +00:00
return strings . Join ( tmpconfig , "" )
2018-04-23 17:30:41 +00:00
}
2018-06-15 03:47:01 +00:00
func getConfig ( config string , ifname string ) string {
2018-04-23 17:30:41 +00:00
tmpconfig := [ ] string { }
2018-05-07 08:25:43 +00:00
config = strings . TrimSpace ( config )
2018-04-23 17:30:41 +00:00
tmpconfig = append ( tmpconfig , config [ : 1 ] )
2018-04-20 17:30:12 +00:00
if ifname != "" {
2018-04-23 17:30:41 +00:00
tmpconfig = append ( tmpconfig , fmt . Sprintf ( ` "ifnameRequest": "%s", ` , ifname ) )
2018-04-20 17:30:12 +00:00
}
2018-04-23 17:30:41 +00:00
tmpconfig = append ( tmpconfig , config [ 1 : ] )
2018-04-27 18:43:20 +00:00
return strings . Join ( tmpconfig , "" )
2018-04-23 17:30:41 +00:00
}
2018-07-12 13:42:56 +00:00
func getNetSpec ( ns types . NetworkAttachmentDefinitionSpec , name string , ifname string ) ( string , error ) {
2018-04-20 17:30:12 +00:00
2018-04-23 17:30:41 +00:00
if ns . Plugin == "" && ns . Config == "" {
return "" , fmt . Errorf ( "Network Object spec plugin and config can't be empty" )
2018-04-20 17:30:12 +00:00
}
2018-04-23 17:30:41 +00:00
if ns . Plugin != "" && ns . Config != "" {
return "" , fmt . Errorf ( "Network Object spec can't have both plugin and config" )
}
if ns . Plugin != "" {
// Plugin contains the name of a CNI plugin on-disk in a
// runtime-defined path (eg /opt/cni/bin and/or other paths.
// This plugin should be executed with a basic CNI JSON
// configuration on stdin containing the Network object
// name and the plugin:
// { “cniVersion”: “0.3.1”, “type”: <Plugin>, “name”: <Network.Name> }
// and any additional “runtimeConfig” field per the
// CNI specification and conventions.
2018-06-15 03:47:01 +00:00
return getPlugin ( ns . Plugin , name , ifname ) , nil
2018-04-23 17:30:41 +00:00
}
// Config contains a standard JSON-encoded CNI configuration
// or configuration list which defines the plugin chain to
// execute. If present, this key takes precedence over
// ‘ Plugin’ .
2018-06-15 03:47:01 +00:00
return getConfig ( ns . Config , ifname ) , nil
2018-04-23 17:30:41 +00:00
}
2018-07-12 13:42:56 +00:00
func cniConfigFromNetworkResource ( customResource * types . NetworkAttachmentDefinition , net * types . NetworkSelectionElement , confdir string ) ( string , error ) {
2018-04-23 17:30:41 +00:00
var config string
var err error
2018-07-12 13:42:56 +00:00
if ( types . NetworkAttachmentDefinitionSpec { } ) == customResource . Spec {
2018-06-20 02:27:42 +00:00
// Network Spec empty; generate delegate from CNI JSON config
// from the configuration directory that has the same network
// name as the custom resource
config , err = getCNIConfig ( customResource . Metadata . Name , net . InterfaceRequest , confdir )
2018-04-23 17:30:41 +00:00
if err != nil {
2018-06-20 02:27:42 +00:00
return "" , fmt . Errorf ( "cniConfigFromNetworkResource: err in getCNIConfig: %v" , err )
2018-04-23 17:30:41 +00:00
}
} else {
2018-06-20 02:27:42 +00:00
// Generate delegate from CNI configuration embedded in the
// custom resource
config , err = getNetSpec ( customResource . Spec , customResource . Metadata . Name , net . InterfaceRequest )
2018-04-23 17:30:41 +00:00
if err != nil {
2018-06-20 02:27:42 +00:00
return "" , fmt . Errorf ( "cniConfigFromNetworkResource: err in getNetSpec: %v" , err )
2018-04-23 17:30:41 +00:00
}
}
2018-04-20 17:30:12 +00:00
2018-04-23 17:30:41 +00:00
return config , nil
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
func getKubernetesDelegate ( client KubeClient , net * types . NetworkSelectionElement , confdir string ) ( * types . DelegateNetConf , error ) {
2018-07-12 13:42:56 +00:00
rawPath := fmt . Sprintf ( "/apis/kubernetes.cni.cncf.io/v1/namespaces/%s/network-attachment-definitions/%s" , net . Namespace , net . Name )
2018-06-20 02:27:42 +00:00
netData , err := client . GetRawWithPath ( rawPath )
2018-04-20 17:30:12 +00:00
if err != nil {
2018-06-20 02:27:42 +00:00
return nil , fmt . Errorf ( "getKubernetesDelegate: failed to get network resource, refer Multus README.md for the usage guide: %v" , err )
2018-04-20 17:30:12 +00:00
}
2018-07-12 13:42:56 +00:00
customResource := & types . NetworkAttachmentDefinition { }
2018-06-20 02:27:42 +00:00
if err := json . Unmarshal ( netData , customResource ) ; err != nil {
return nil , fmt . Errorf ( "getKubernetesDelegate: failed to get the netplugin data: %v" , err )
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
cniConfig , err := cniConfigFromNetworkResource ( customResource , net , confdir )
2018-04-20 17:30:12 +00:00
if err != nil {
2018-06-20 02:27:42 +00:00
return nil , err
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
delegate , err := types . LoadDelegateNetConf ( [ ] byte ( cniConfig ) )
2018-05-01 18:08:39 +00:00
if err != nil {
2018-06-20 02:27:42 +00:00
return nil , err
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
return delegate , nil
2018-04-20 17:30:12 +00:00
}
2018-06-14 18:15:02 +00:00
type KubeClient interface {
GetRawWithPath ( path string ) ( [ ] byte , error )
GetPod ( namespace , name string ) ( * v1 . Pod , error )
}
func GetK8sNetwork ( args * skel . CmdArgs , kubeconfig string , k8sclient KubeClient , confdir string ) ( [ ] * types . DelegateNetConf , error ) {
2018-04-20 17:30:12 +00:00
k8sArgs := types . K8sArgs { }
err := cnitypes . LoadArgs ( args . Args , & k8sArgs )
if err != nil {
2018-05-01 18:08:39 +00:00
return nil , err
2018-04-20 17:30:12 +00:00
}
2018-06-14 18:15:02 +00:00
if k8sclient == nil {
k8sclient , err = createK8sClient ( kubeconfig )
if err != nil {
return nil , err
}
2018-04-20 17:30:12 +00:00
}
2018-06-15 03:31:22 +00:00
netAnnot , defaultNamespace , err := getPodNetworkAnnotation ( k8sclient , k8sArgs )
2018-04-20 17:30:12 +00:00
if err != nil {
2018-05-01 18:08:39 +00:00
return nil , err
2018-04-20 17:30:12 +00:00
}
if len ( netAnnot ) == 0 {
2018-05-01 18:08:39 +00:00
return nil , & NoK8sNetworkError { "no kubernetes network found" }
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
networks , err := parsePodNetworkAnnotation ( netAnnot , defaultNamespace )
2018-04-20 17:30:12 +00:00
if err != nil {
2018-05-01 18:08:39 +00:00
return nil , err
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
// Read all network objects referenced by 'networks'
var delegates [ ] * types . DelegateNetConf
for _ , net := range networks {
delegate , err := getKubernetesDelegate ( k8sclient , net , confdir )
if err != nil {
return nil , fmt . Errorf ( "GetK8sNetwork: failed getting the delegate: %v" , err )
}
delegates = append ( delegates , delegate )
2018-04-20 17:30:12 +00:00
}
2018-06-20 02:27:42 +00:00
return delegates , nil
2018-04-20 17:30:12 +00:00
}