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-07-19 14:02:49 +00:00
"os"
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
2018-07-30 10:59:13 +00:00
v1 "k8s.io/api/core/v1"
2018-04-20 17:30:12 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
2018-07-19 14:02:49 +00:00
"k8s.io/client-go/rest"
2018-04-20 17:30:12 +00:00
"k8s.io/client-go/tools/clientcmd"
2018-07-30 10:59:13 +00:00
"k8s.io/client-go/util/retry"
2018-04-20 17:30:12 +00:00
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-07-30 10:59:13 +00:00
type clientInfo struct {
Client KubeClient
Podnamespace string
Podname string
}
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 { } )
}
2018-07-30 10:59:13 +00:00
func ( d * defaultKubeClient ) UpdatePodStatus ( pod * v1 . Pod ) ( * v1 . Pod , error ) {
return d . client . Core ( ) . Pods ( pod . Namespace ) . UpdateStatus ( pod )
}
func setKubeClientInfo ( c * clientInfo , client KubeClient , k8sArgs * types . K8sArgs ) {
c . Client = client
c . Podnamespace = string ( k8sArgs . K8S_POD_NAMESPACE )
c . Podname = string ( k8sArgs . K8S_POD_NAME )
}
func SetNetworkStatus ( k * clientInfo , netStatus [ ] * types . NetworkStatus ) error {
pod , err := k . Client . GetPod ( k . Podnamespace , k . Podname )
if err != nil {
return fmt . Errorf ( "SetNetworkStatus: failed to query the pod %v in out of cluster comm: %v" , k . Podname , err )
}
var ns string
if netStatus != nil {
var networkStatus [ ] string
for _ , nets := range netStatus {
data , err := json . MarshalIndent ( nets , "" , " " )
if err != nil {
return fmt . Errorf ( "SetNetworkStatus: error with Marshal Indent: %v" , err )
}
networkStatus = append ( networkStatus , string ( data ) )
}
ns = fmt . Sprintf ( "[%s]" , strings . Join ( networkStatus , "," ) )
}
_ , err = setPodNetworkAnnotation ( k . Client , k . Podnamespace , pod , ns )
if err != nil {
return fmt . Errorf ( "SetNetworkStatus: failed to update the pod %v in out of cluster comm: %v" , k . Podname , err )
}
return nil
}
func setPodNetworkAnnotation ( client KubeClient , namespace string , pod * v1 . Pod , networkstatus string ) ( * v1 . Pod , error ) {
//if pod annotations is empty, make sure it allocatable
if len ( pod . Annotations ) == 0 {
pod . Annotations = make ( map [ string ] string )
}
pod . Annotations [ "k8s.v1.cni.cncf.io/networks-status" ] = networkstatus
pod = pod . DeepCopy ( )
var err error
if resultErr := retry . RetryOnConflict ( retry . DefaultBackoff , func ( ) error {
if err != nil {
// Re-get the pod unless it's the first attempt to update
pod , err = client . GetPod ( pod . Namespace , pod . Name )
if err != nil {
return err
}
}
pod , err = client . UpdatePodStatus ( pod )
return err
} ) ; resultErr != nil {
return nil , fmt . Errorf ( "status update failed for pod %s/%s: %v" , pod . Namespace , pod . Name , resultErr )
}
return pod , nil
}
2018-07-26 23:04:52 +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-07-19 21:57:08 +00:00
return pod . Annotations [ "k8s.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-07-19 16:21:31 +00:00
func getCNIConfigFromFile ( name string , confdir string ) ( [ ] byte , 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
// In part, adapted from K8s pkg/kubelet/dockershim/network/cni/cni.go#getDefaultCNINetwork
2018-07-26 23:04:52 +00:00
files , err := libcni . ConfFiles ( confdir , [ ] string { ".conf" , ".json" , ".conflist" } )
2018-04-27 18:43:20 +00:00
switch {
case err != nil :
2018-07-19 16:21:31 +00:00
return nil , fmt . Errorf ( "No networks found in %s" , confdir )
2018-04-27 18:43:20 +00:00
case len ( files ) == 0 :
2018-07-19 16:21:31 +00:00
return nil , fmt . Errorf ( "No networks found in %s" , confdir )
2018-04-27 18:43:20 +00:00
}
for _ , confFile := range files {
2018-07-26 23:04:52 +00:00
var confList * libcni . NetworkConfigList
if strings . HasSuffix ( confFile , ".conflist" ) {
confList , err = libcni . ConfListFromFile ( confFile )
if err != nil {
return nil , fmt . Errorf ( "Error loading CNI conflist file %s: %v" , confFile , err )
}
if confList . Name == name {
return confList . Bytes , nil
}
2018-04-27 18:43:20 +00:00
2018-07-26 23:04:52 +00:00
} else {
conf , err := libcni . ConfFromFile ( confFile )
if err != nil {
return nil , 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 nil , fmt . Errorf ( "Error loading CNI config file %s: no 'type'; perhaps this is a .conflist?" , confFile )
}
return conf . Bytes , nil
2018-04-27 18:43:20 +00:00
}
}
}
2018-07-19 16:21:31 +00:00
return nil , fmt . Errorf ( "no network available in the name %s in cni dir %s" , name , confdir )
2018-04-23 17:30:41 +00:00
}
2018-07-19 16:21:31 +00:00
// getCNIConfigFromSpec reads a CNI JSON configuration from the NetworkAttachmentDefinition
// object's Spec.Config field and fills in any missing details like the network name
func getCNIConfigFromSpec ( configData , netName string ) ( [ ] byte , error ) {
var rawConfig map [ string ] interface { }
var err error
2018-04-20 17:30:12 +00:00
2018-07-19 16:21:31 +00:00
configBytes := [ ] byte ( configData )
err = json . Unmarshal ( configBytes , & rawConfig )
if err != nil {
return nil , fmt . Errorf ( "getCNIConfigFromSpec: failed to unmarshal Spec.Config: %v" , err )
2018-04-23 17:30:41 +00:00
}
2018-07-19 16:21:31 +00:00
// Inject network name if missing from Config for the thick plugin case
if n , ok := rawConfig [ "name" ] ; ! ok || n == "" {
rawConfig [ "name" ] = netName
configBytes , err = json . Marshal ( rawConfig )
if err != nil {
return nil , fmt . Errorf ( "getCNIConfigFromSpec: failed to re-marshal Spec.Config: %v" , err )
}
2018-04-23 17:30:41 +00:00
}
2018-07-19 16:21:31 +00:00
return configBytes , nil
2018-04-23 17:30:41 +00:00
}
2018-07-19 16:21:31 +00:00
func cniConfigFromNetworkResource ( customResource * types . NetworkAttachmentDefinition , confdir string ) ( [ ] byte , error ) {
var config [ ] byte
2018-04-23 17:30:41 +00:00
var err error
2018-07-19 16:21:31 +00:00
emptySpec := types . NetworkAttachmentDefinitionSpec { }
2018-07-26 23:04:52 +00:00
if customResource . Spec == emptySpec {
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
2018-07-19 16:21:31 +00:00
config , err = getCNIConfigFromFile ( customResource . Metadata . Name , confdir )
2018-04-23 17:30:41 +00:00
if err != nil {
2018-07-19 16:21:31 +00:00
return nil , fmt . Errorf ( "cniConfigFromNetworkResource: err in getCNIConfigFromFile: %v" , err )
2018-04-23 17:30:41 +00:00
}
} else {
2018-07-19 16:21:31 +00:00
// Config contains a standard JSON-encoded CNI configuration
// or configuration list which defines the plugin chain to
// execute.
config , err = getCNIConfigFromSpec ( customResource . Spec . Config , customResource . Metadata . Name )
2018-04-23 17:30:41 +00:00
if err != nil {
2018-07-19 16:21:31 +00:00
return nil , fmt . Errorf ( "cniConfigFromNetworkResource: err in getCNIConfigFromSpec: %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-19 21:57:08 +00:00
rawPath := fmt . Sprintf ( "/apis/k8s.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-07-19 16:21:31 +00:00
configBytes , err := cniConfigFromNetworkResource ( customResource , 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-07-19 16:21:31 +00:00
delegate , err := types . LoadDelegateNetConf ( configBytes , net . InterfaceRequest )
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 )
2018-07-30 10:59:13 +00:00
UpdatePodStatus ( pod * v1 . Pod ) ( * v1 . Pod , error )
2018-06-14 18:15:02 +00:00
}
2018-07-26 23:04:52 +00:00
func GetK8sArgs ( args * skel . CmdArgs ) ( * types . K8sArgs , error ) {
k8sArgs := & types . K8sArgs { }
err := cnitypes . LoadArgs ( args . Args , k8sArgs )
if err != nil {
return nil , err
}
return k8sArgs , nil
}
2018-07-30 10:59:13 +00:00
// Attempts to load Kubernetes-defined delegates and add them to the Multus config.
// Returns the number of Kubernetes-defined delegates added or an error.
func TryLoadK8sDelegates ( k8sArgs * types . K8sArgs , conf * types . NetConf , kubeClient KubeClient ) ( int , * clientInfo , error ) {
var err error
clientInfo := & clientInfo { }
kubeClient , err = GetK8sClient ( conf . Kubeconfig , kubeClient )
if err != nil {
return 0 , nil , err
}
if kubeClient == nil {
if len ( conf . Delegates ) == 0 {
// No available kube client and no delegates, we can't do anything
return 0 , nil , fmt . Errorf ( "must have either Kubernetes config or delegates, refer Multus README.md for the usage guide" )
}
return 0 , nil , nil
}
setKubeClientInfo ( clientInfo , kubeClient , k8sArgs )
delegates , err := GetK8sNetwork ( kubeClient , k8sArgs , conf . ConfDir )
if err != nil {
if _ , ok := err . ( * NoK8sNetworkError ) ; ok {
2018-08-01 13:54:10 +00:00
return 0 , clientInfo , nil
2018-07-30 10:59:13 +00:00
}
return 0 , nil , fmt . Errorf ( "Multus: Err in getting k8s network from pod: %v" , err )
}
if err = conf . AddDelegates ( delegates ) ; err != nil {
return 0 , nil , err
}
return len ( delegates ) , clientInfo , nil
}
2018-07-19 14:02:49 +00:00
func GetK8sClient ( kubeconfig string , kubeClient KubeClient ) ( KubeClient , error ) {
// If we get a valid kubeClient (eg from testcases) just return that
// one.
if kubeClient != nil {
return kubeClient , nil
}
2018-04-20 17:30:12 +00:00
2018-07-19 14:02:49 +00:00
var err error
var config * rest . Config
// Otherwise try to create a kubeClient from a given kubeConfig
if kubeconfig != "" {
// uses the current context in kubeconfig
config , err = clientcmd . BuildConfigFromFlags ( "" , kubeconfig )
if err != nil {
return nil , fmt . Errorf ( "GetK8sClient: failed to get context for the kubeconfig %v, refer Multus README.md for the usage guide: %v" , kubeconfig , err )
}
} else if os . Getenv ( "KUBERNETES_SERVICE_HOST" ) != "" && os . Getenv ( "KUBERNETES_SERVICE_PORT" ) != "" {
// Try in-cluster config where multus might be running in a kubernetes pod
config , err = rest . InClusterConfig ( )
if err != nil {
return nil , fmt . Errorf ( "createK8sClient: failed to get context for in-cluster kube config, refer Multus README.md for the usage guide: %v" , err )
}
} else {
// No kubernetes config; assume we shouldn't talk to Kube at all
return nil , nil
}
// creates the clientset
client , err := kubernetes . NewForConfig ( config )
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-07-19 14:02:49 +00:00
return & defaultKubeClient { client : client } , nil
}
2018-07-26 23:04:52 +00:00
func GetK8sNetwork ( k8sclient KubeClient , k8sArgs * types . K8sArgs , confdir string ) ( [ ] * types . DelegateNetConf , error ) {
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
}