mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 17:30:00 +00:00
Merge pull request #13857 from mesosphere/node-labels
Auto commit by PR queue bot
This commit is contained in:
commit
c807bea089
@ -33,6 +33,7 @@ import (
|
|||||||
mutil "github.com/mesos/mesos-go/mesosutil"
|
mutil "github.com/mesos/mesos-go/mesosutil"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/archive"
|
"k8s.io/kubernetes/contrib/mesos/pkg/archive"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/executor/messages"
|
"k8s.io/kubernetes/contrib/mesos/pkg/executor/messages"
|
||||||
|
"k8s.io/kubernetes/contrib/mesos/pkg/node"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/meta"
|
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/meta"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/client/cache"
|
"k8s.io/kubernetes/pkg/client/cache"
|
||||||
@ -225,6 +226,13 @@ func (k *KubernetesExecutor) Registered(driver bindings.ExecutorDriver,
|
|||||||
k.staticPodsConfig = executorInfo.Data
|
k.staticPodsConfig = executorInfo.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if slaveInfo != nil {
|
||||||
|
_, err := node.CreateOrUpdate(k.client, slaveInfo.GetHostname(), node.SlaveAttributesToLabels(slaveInfo.Attributes))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("cannot update node labels: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
k.initialRegistration.Do(k.onInitialRegistration)
|
k.initialRegistration.Do(k.onInitialRegistration)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,11 +247,19 @@ func (k *KubernetesExecutor) Reregistered(driver bindings.ExecutorDriver, slaveI
|
|||||||
log.Errorf("failed to reregister/transition to a connected state")
|
log.Errorf("failed to reregister/transition to a connected state")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if slaveInfo != nil {
|
||||||
|
_, err := node.CreateOrUpdate(k.client, slaveInfo.GetHostname(), node.SlaveAttributesToLabels(slaveInfo.Attributes))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("cannot update node labels: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
k.initialRegistration.Do(k.onInitialRegistration)
|
k.initialRegistration.Do(k.onInitialRegistration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *KubernetesExecutor) onInitialRegistration() {
|
func (k *KubernetesExecutor) onInitialRegistration() {
|
||||||
defer close(k.initialRegComplete)
|
defer close(k.initialRegComplete)
|
||||||
|
|
||||||
// emit an empty update to allow the mesos "source" to be marked as seen
|
// emit an empty update to allow the mesos "source" to be marked as seen
|
||||||
k.updateChan <- kubelet.PodUpdate{
|
k.updateChan <- kubelet.PodUpdate{
|
||||||
Pods: []*api.Pod{},
|
Pods: []*api.Pod{},
|
||||||
|
18
contrib/mesos/pkg/node/doc.go
Normal file
18
contrib/mesos/pkg/node/doc.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 node provides utilities to create and update nodes
|
||||||
|
package node
|
160
contrib/mesos/pkg/node/node.go
Normal file
160
contrib/mesos/pkg/node/node.go
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
mesos "github.com/mesos/mesos-go/mesosproto"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/api/errors"
|
||||||
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/util/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
labelPrefix = "k8s.mesosphere.io/attribute-"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create creates a new node api object with the given hostname and labels
|
||||||
|
func Create(client *client.Client, hostName string, labels map[string]string) (*api.Node, error) {
|
||||||
|
n := api.Node{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: hostName,
|
||||||
|
Labels: map[string]string{"kubernetes.io/hostname": hostName},
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: hostName,
|
||||||
|
},
|
||||||
|
Status: api.NodeStatus{
|
||||||
|
Phase: api.NodePending,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for k, v := range labels {
|
||||||
|
n.Labels[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to create
|
||||||
|
return client.Nodes().Create(&n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing node api object with new labels
|
||||||
|
func Update(client *client.Client, n *api.Node, labels map[string]string) (*api.Node, error) {
|
||||||
|
patch := struct {
|
||||||
|
Metadata struct {
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
} `json:"metadata"`
|
||||||
|
}{}
|
||||||
|
patch.Metadata.Labels = map[string]string{}
|
||||||
|
for k, v := range n.Labels {
|
||||||
|
if !IsSlaveAttributeLabel(k) {
|
||||||
|
patch.Metadata.Labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range labels {
|
||||||
|
patch.Metadata.Labels[k] = v
|
||||||
|
}
|
||||||
|
patchJson, _ := json.Marshal(patch)
|
||||||
|
log.V(4).Infof("Patching labels of node %q: %v", n.Name, string(patchJson))
|
||||||
|
err := client.Patch(api.MergePatchType).RequestURI(n.SelfLink).Body(patchJson).Do().Error()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating labels of node %q: %v", n.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newNode, err := api.Scheme.DeepCopy(n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newNode.(*api.Node).Labels = patch.Metadata.Labels
|
||||||
|
|
||||||
|
return newNode.(*api.Node), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdate tries to create a node api object or updates an already existing one
|
||||||
|
func CreateOrUpdate(client *client.Client, hostName string, labels map[string]string) (*api.Node, error) {
|
||||||
|
n, err := Create(client, hostName, labels)
|
||||||
|
if err == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
if !errors.IsAlreadyExists(err) {
|
||||||
|
return nil, fmt.Errorf("unable to register %q with the apiserver: %v", hostName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fall back to update an old node with new labels
|
||||||
|
n, err = client.Nodes().Get(hostName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting node %q: %v", hostName, err)
|
||||||
|
}
|
||||||
|
if n == nil {
|
||||||
|
return nil, fmt.Errorf("no node instance returned for %q", hostName)
|
||||||
|
}
|
||||||
|
return Update(client, n, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSlaveAttributeLabel returns true iff the given label is derived from a slave attribute
|
||||||
|
func IsSlaveAttributeLabel(l string) bool {
|
||||||
|
return strings.HasPrefix(l, labelPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUpToDate returns true iff the node's slave labels match the given attributes labels
|
||||||
|
func IsUpToDate(n *api.Node, labels map[string]string) bool {
|
||||||
|
slaveLabels := map[string]string{}
|
||||||
|
for k, v := range n.Labels {
|
||||||
|
if IsSlaveAttributeLabel(k) {
|
||||||
|
slaveLabels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(slaveLabels, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlaveAttributesToLabels converts slave attributes into string key/value labels
|
||||||
|
func SlaveAttributesToLabels(attrs []*mesos.Attribute) map[string]string {
|
||||||
|
l := map[string]string{}
|
||||||
|
for _, a := range attrs {
|
||||||
|
if a == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var v string
|
||||||
|
k := labelPrefix + a.GetName()
|
||||||
|
|
||||||
|
switch a.GetType() {
|
||||||
|
case mesos.Value_TEXT:
|
||||||
|
v = a.GetText().GetValue()
|
||||||
|
case mesos.Value_SCALAR:
|
||||||
|
v = strconv.FormatFloat(a.GetScalar().GetValue(), 'G', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validation.IsQualifiedName(k) {
|
||||||
|
log.V(3).Infof("ignoring invalid node label name %q", k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validation.IsValidLabelValue(v) {
|
||||||
|
log.V(3).Infof("ignoring invalid node label %s value: %q", k, v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l[k] = v
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
148
contrib/mesos/pkg/node/registrator.go
Normal file
148
contrib/mesos/pkg/node/registrator.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 node
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"k8s.io/kubernetes/contrib/mesos/pkg/queue"
|
||||||
|
"k8s.io/kubernetes/contrib/mesos/pkg/runtime"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/api/errors"
|
||||||
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registrator interface {
|
||||||
|
// Register checks whether the node is registered with the given labels. If it
|
||||||
|
// is not, it is created or updated on the apiserver. If an the node was up-to-date,
|
||||||
|
// false is returned.
|
||||||
|
Register(hostName string, labels map[string]string) (bool, error)
|
||||||
|
|
||||||
|
// Start the registration loop and return immediately.
|
||||||
|
Run(terminate <-chan struct{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type registration struct {
|
||||||
|
hostName string
|
||||||
|
labels map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registration) Copy() queue.Copyable {
|
||||||
|
return ®istration{
|
||||||
|
hostName: r.hostName,
|
||||||
|
labels: r.labels, // labels are never changed, no need to clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registration) GetUID() string {
|
||||||
|
return r.hostName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registration) Value() queue.UniqueCopyable {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type LookupFunc func(hostName string) *api.Node
|
||||||
|
|
||||||
|
type clientRegistrator struct {
|
||||||
|
lookupNode LookupFunc
|
||||||
|
client *client.Client
|
||||||
|
queue *queue.HistoricalFIFO
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistrator(client *client.Client, lookupNode LookupFunc) *clientRegistrator {
|
||||||
|
return &clientRegistrator{
|
||||||
|
lookupNode: lookupNode,
|
||||||
|
client: client,
|
||||||
|
queue: queue.NewHistorical(nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *clientRegistrator) Run(terminate <-chan struct{}) error {
|
||||||
|
loop := func() {
|
||||||
|
RegistrationLoop:
|
||||||
|
for {
|
||||||
|
obj := r.queue.CancelablePop(terminate)
|
||||||
|
if obj == nil {
|
||||||
|
break RegistrationLoop
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-terminate:
|
||||||
|
break RegistrationLoop
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
rg := obj.(*registration)
|
||||||
|
n, needsUpdate := r.updateNecessary(rg.hostName, rg.labels)
|
||||||
|
if !needsUpdate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == nil {
|
||||||
|
log.V(2).Infof("creating node %s with labels %v", rg.hostName, rg.labels)
|
||||||
|
_, err := CreateOrUpdate(r.client, rg.hostName, rg.labels)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error creating the node %s: %v", rg.hostName, rg.labels)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.V(2).Infof("updating node %s with labels %v", rg.hostName, rg.labels)
|
||||||
|
_, err := Update(r.client, n, rg.labels)
|
||||||
|
if err != nil && errors.IsNotFound(err) {
|
||||||
|
// last chance when our store was out of date
|
||||||
|
_, err = Create(r.client, rg.hostName, rg.labels)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error updating the node %s: %v", rg.hostName, rg.labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go runtime.Until(loop, time.Second, terminate)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *clientRegistrator) Register(hostName string, labels map[string]string) (bool, error) {
|
||||||
|
_, needsUpdate := r.updateNecessary(hostName, labels)
|
||||||
|
|
||||||
|
if needsUpdate {
|
||||||
|
log.V(5).Infof("queuing registration for node %s with labels %v", hostName, labels)
|
||||||
|
err := r.queue.Update(®istration{
|
||||||
|
hostName: hostName,
|
||||||
|
labels: labels,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("cannot register node %s: %v", hostName, err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateNecessary retrieves the node with the given hostname and checks whether the given
|
||||||
|
// labels would mean any update to the node. The unmodified node is returned, plus
|
||||||
|
// true iff an update is necessary.
|
||||||
|
func (r *clientRegistrator) updateNecessary(hostName string, labels map[string]string) (*api.Node, bool) {
|
||||||
|
if r.lookupNode == nil {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
n := r.lookupNode(hostName)
|
||||||
|
return n, n == nil || !IsUpToDate(n, labels)
|
||||||
|
}
|
@ -222,7 +222,7 @@ func (f *HistoricalFIFO) Poll(id string, t EventType) bool {
|
|||||||
func (q *HistoricalFIFO) Await(timeout time.Duration) interface{} {
|
func (q *HistoricalFIFO) Await(timeout time.Duration) interface{} {
|
||||||
cancel := make(chan struct{})
|
cancel := make(chan struct{})
|
||||||
ch := make(chan interface{}, 1)
|
ch := make(chan interface{}, 1)
|
||||||
go func() { ch <- q.pop(cancel) }()
|
go func() { ch <- q.CancelablePop(cancel) }()
|
||||||
select {
|
select {
|
||||||
case <-time.After(timeout):
|
case <-time.After(timeout):
|
||||||
close(cancel)
|
close(cancel)
|
||||||
@ -232,10 +232,10 @@ func (q *HistoricalFIFO) Await(timeout time.Duration) interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (f *HistoricalFIFO) Pop() interface{} {
|
func (f *HistoricalFIFO) Pop() interface{} {
|
||||||
return f.pop(nil)
|
return f.CancelablePop(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *HistoricalFIFO) pop(cancel chan struct{}) interface{} {
|
func (f *HistoricalFIFO) CancelablePop(cancel <-chan struct{}) interface{} {
|
||||||
popEvent := (Entry)(nil)
|
popEvent := (Entry)(nil)
|
||||||
defer func() {
|
defer func() {
|
||||||
f.carrier(popEvent)
|
f.carrier(popEvent)
|
||||||
@ -383,7 +383,7 @@ func (f *HistoricalFIFO) merge(id string, obj UniqueCopyable) (notifications []E
|
|||||||
// NewHistorical returns a Store which can be used to queue up items to
|
// NewHistorical returns a Store which can be used to queue up items to
|
||||||
// process. If a non-nil Mux is provided, then modifications to the
|
// process. If a non-nil Mux is provided, then modifications to the
|
||||||
// the FIFO are delivered on a channel specific to this fifo.
|
// the FIFO are delivered on a channel specific to this fifo.
|
||||||
func NewHistorical(ch chan<- Entry) FIFO {
|
func NewHistorical(ch chan<- Entry) *HistoricalFIFO {
|
||||||
carrier := dead
|
carrier := dead
|
||||||
if ch != nil {
|
if ch != nil {
|
||||||
carrier = func(msg Entry) {
|
carrier = func(msg Entry) {
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/contrib/mesos/pkg/node"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/offers"
|
"k8s.io/kubernetes/contrib/mesos/pkg/offers"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/podtask"
|
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/podtask"
|
||||||
)
|
)
|
||||||
@ -53,10 +54,11 @@ func NewAllocationStrategy(fitPredicate podtask.FitPredicate, procurement podtas
|
|||||||
|
|
||||||
type fcfsPodScheduler struct {
|
type fcfsPodScheduler struct {
|
||||||
AllocationStrategy
|
AllocationStrategy
|
||||||
|
lookupNode node.LookupFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFCFSPodScheduler(as AllocationStrategy) PodScheduler {
|
func NewFCFSPodScheduler(as AllocationStrategy, lookupNode node.LookupFunc) PodScheduler {
|
||||||
return &fcfsPodScheduler{as}
|
return &fcfsPodScheduler{as, lookupNode}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A first-come-first-serve scheduler: acquires the first offer that can support the task
|
// A first-come-first-serve scheduler: acquires the first offer that can support the task
|
||||||
@ -68,7 +70,18 @@ func (fps *fcfsPodScheduler) SchedulePod(r offers.Registry, unused SlaveIndex, t
|
|||||||
if offer == nil {
|
if offer == nil {
|
||||||
return false, fmt.Errorf("nil offer while scheduling task %v", task.ID)
|
return false, fmt.Errorf("nil offer while scheduling task %v", task.ID)
|
||||||
}
|
}
|
||||||
if fps.FitPredicate()(task, offer) {
|
|
||||||
|
// check that the node actually exists. As offers are declined if not, the
|
||||||
|
// case n==nil can only happen when the node object was deleted since the
|
||||||
|
// offer came in.
|
||||||
|
nodeName := offer.GetHostname()
|
||||||
|
n := fps.lookupNode(nodeName)
|
||||||
|
if n == nil {
|
||||||
|
log.V(3).Infof("ignoring offer for node %s because node went away", nodeName)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fps.FitPredicate()(task, offer, n) {
|
||||||
if p.Acquire() {
|
if p.Acquire() {
|
||||||
acceptedOffer = p
|
acceptedOffer = p
|
||||||
log.V(3).Infof("Pod %s accepted offer %v", podName, offer.Id.GetValue())
|
log.V(3).Infof("Pod %s accepted offer %v", podName, offer.Id.GetValue())
|
||||||
|
@ -543,7 +543,11 @@ func (k *errorHandler) handleSchedulingError(pod *api.Pod, schedulingErr error)
|
|||||||
defer k.api.Unlock()
|
defer k.api.Unlock()
|
||||||
switch task, state := k.api.tasks().Get(task.ID); state {
|
switch task, state := k.api.tasks().Get(task.ID); state {
|
||||||
case podtask.StatePending:
|
case podtask.StatePending:
|
||||||
return !task.Has(podtask.Launched) && k.api.algorithm().FitPredicate()(task, offer)
|
// Assess fitness of pod with the current offer. The scheduler normally
|
||||||
|
// "backs off" when it can't find an offer that matches up with a pod.
|
||||||
|
// The backoff period for a pod can terminate sooner if an offer becomes
|
||||||
|
// available that matches up.
|
||||||
|
return !task.Has(podtask.Launched) && k.api.algorithm().FitPredicate()(task, offer, nil)
|
||||||
default:
|
default:
|
||||||
// no point in continuing to check for matching offers
|
// no point in continuing to check for matching offers
|
||||||
return true
|
return true
|
||||||
|
@ -393,13 +393,20 @@ func TestPlugin_LifeCycle(t *testing.T) {
|
|||||||
executor.Data = []byte{0, 1, 2}
|
executor.Data = []byte{0, 1, 2}
|
||||||
|
|
||||||
// create scheduler
|
// create scheduler
|
||||||
|
nodeStore := cache.NewStore(cache.MetaNamespaceKeyFunc)
|
||||||
as := NewAllocationStrategy(
|
as := NewAllocationStrategy(
|
||||||
podtask.DefaultPredicate,
|
podtask.DefaultPredicate,
|
||||||
podtask.NewDefaultProcurement(mresource.DefaultDefaultContainerCPULimit, mresource.DefaultDefaultContainerMemLimit))
|
podtask.NewDefaultProcurement(mresource.DefaultDefaultContainerCPULimit, mresource.DefaultDefaultContainerMemLimit))
|
||||||
testScheduler := New(Config{
|
testScheduler := New(Config{
|
||||||
Executor: executor,
|
Executor: executor,
|
||||||
Client: client.NewOrDie(&client.Config{Host: testApiServer.server.URL, Version: testapi.Default.Version()}),
|
Client: client.NewOrDie(&client.Config{Host: testApiServer.server.URL, Version: testapi.Default.Version()}),
|
||||||
Scheduler: NewFCFSPodScheduler(as),
|
Scheduler: NewFCFSPodScheduler(as, func(node string) *api.Node {
|
||||||
|
obj, _, _ := nodeStore.GetByKey(node)
|
||||||
|
if obj == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return obj.(*api.Node)
|
||||||
|
}),
|
||||||
Schedcfg: *schedcfg.CreateDefaultConfig(),
|
Schedcfg: *schedcfg.CreateDefaultConfig(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ package podtask
|
|||||||
import (
|
import (
|
||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
mesos "github.com/mesos/mesos-go/mesosproto"
|
mesos "github.com/mesos/mesos-go/mesosproto"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// bogus numbers that we use to make sure that there's some set of minimal offered resources on the slave
|
// bogus numbers that we use to make sure that there's some set of minimal offered resources on the slave
|
||||||
@ -43,7 +44,7 @@ var (
|
|||||||
}).Procure
|
}).Procure
|
||||||
)
|
)
|
||||||
|
|
||||||
func MinimalPodResourcesPredicate(t *T, offer *mesos.Offer) bool {
|
func MinimalPodResourcesPredicate(t *T, offer *mesos.Offer, _ *api.Node) bool {
|
||||||
var (
|
var (
|
||||||
offeredCpus float64
|
offeredCpus float64
|
||||||
offeredMem float64
|
offeredMem float64
|
||||||
|
@ -19,14 +19,14 @@ package podtask
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gogo/protobuf/proto"
|
||||||
mesos "github.com/mesos/mesos-go/mesosproto"
|
mesos "github.com/mesos/mesos-go/mesosproto"
|
||||||
mutil "github.com/mesos/mesos-go/mesosutil"
|
mutil "github.com/mesos/mesos-go/mesosutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/kubernetes/contrib/mesos/pkg/node"
|
||||||
mresource "k8s.io/kubernetes/contrib/mesos/pkg/scheduler/resource"
|
mresource "k8s.io/kubernetes/contrib/mesos/pkg/scheduler/resource"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/api/resource"
|
"k8s.io/kubernetes/pkg/api/resource"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -146,10 +146,10 @@ func TestEmptyOffer(t *testing.T) {
|
|||||||
mresource.LimitPodCPU(&task.Pod, mresource.DefaultDefaultContainerCPULimit)
|
mresource.LimitPodCPU(&task.Pod, mresource.DefaultDefaultContainerCPULimit)
|
||||||
mresource.LimitPodMem(&task.Pod, mresource.DefaultDefaultContainerMemLimit)
|
mresource.LimitPodMem(&task.Pod, mresource.DefaultDefaultContainerMemLimit)
|
||||||
|
|
||||||
if ok := DefaultPredicate(task, nil); ok {
|
if ok := DefaultPredicate(task, nil, nil); ok {
|
||||||
t.Fatalf("accepted nil offer")
|
t.Fatalf("accepted nil offer")
|
||||||
}
|
}
|
||||||
if ok := DefaultPredicate(task, &mesos.Offer{}); ok {
|
if ok := DefaultPredicate(task, &mesos.Offer{}, nil); ok {
|
||||||
t.Fatalf("accepted empty offer")
|
t.Fatalf("accepted empty offer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,7 +176,7 @@ func TestNoPortsInPodOrOffer(t *testing.T) {
|
|||||||
mutil.NewScalarResource("mem", 0.001),
|
mutil.NewScalarResource("mem", 0.001),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if ok := DefaultPredicate(task, offer); ok {
|
if ok := DefaultPredicate(task, offer, nil); ok {
|
||||||
t.Fatalf("accepted offer %v:", offer)
|
t.Fatalf("accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ func TestNoPortsInPodOrOffer(t *testing.T) {
|
|||||||
mutil.NewScalarResource("mem", t_min_mem),
|
mutil.NewScalarResource("mem", t_min_mem),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if ok := DefaultPredicate(task, offer); !ok {
|
if ok := DefaultPredicate(task, offer, nil); !ok {
|
||||||
t.Fatalf("did not accepted offer %v:", offer)
|
t.Fatalf("did not accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,7 +203,7 @@ func TestAcceptOfferPorts(t *testing.T) {
|
|||||||
rangeResource("ports", []uint64{1, 1}),
|
rangeResource("ports", []uint64{1, 1}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if ok := DefaultPredicate(task, offer); !ok {
|
if ok := DefaultPredicate(task, offer, nil); !ok {
|
||||||
t.Fatalf("did not accepted offer %v:", offer)
|
t.Fatalf("did not accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,17 +218,17 @@ func TestAcceptOfferPorts(t *testing.T) {
|
|||||||
mresource.LimitPodCPU(&task.Pod, mresource.DefaultDefaultContainerCPULimit)
|
mresource.LimitPodCPU(&task.Pod, mresource.DefaultDefaultContainerCPULimit)
|
||||||
mresource.LimitPodMem(&task.Pod, mresource.DefaultDefaultContainerMemLimit)
|
mresource.LimitPodMem(&task.Pod, mresource.DefaultDefaultContainerMemLimit)
|
||||||
|
|
||||||
if ok := DefaultPredicate(task, offer); ok {
|
if ok := DefaultPredicate(task, offer, nil); ok {
|
||||||
t.Fatalf("accepted offer %v:", offer)
|
t.Fatalf("accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pod.Spec.Containers[0].Ports[0].HostPort = 1
|
pod.Spec.Containers[0].Ports[0].HostPort = 1
|
||||||
if ok := DefaultPredicate(task, offer); !ok {
|
if ok := DefaultPredicate(task, offer, nil); !ok {
|
||||||
t.Fatalf("did not accepted offer %v:", offer)
|
t.Fatalf("did not accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pod.Spec.Containers[0].Ports[0].HostPort = 0
|
pod.Spec.Containers[0].Ports[0].HostPort = 0
|
||||||
if ok := DefaultPredicate(task, offer); !ok {
|
if ok := DefaultPredicate(task, offer, nil); !ok {
|
||||||
t.Fatalf("did not accepted offer %v:", offer)
|
t.Fatalf("did not accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,12 +236,12 @@ func TestAcceptOfferPorts(t *testing.T) {
|
|||||||
mutil.NewScalarResource("cpus", t_min_cpu),
|
mutil.NewScalarResource("cpus", t_min_cpu),
|
||||||
mutil.NewScalarResource("mem", t_min_mem),
|
mutil.NewScalarResource("mem", t_min_mem),
|
||||||
}
|
}
|
||||||
if ok := DefaultPredicate(task, offer); ok {
|
if ok := DefaultPredicate(task, offer, nil); ok {
|
||||||
t.Fatalf("accepted offer %v:", offer)
|
t.Fatalf("accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pod.Spec.Containers[0].Ports[0].HostPort = 1
|
pod.Spec.Containers[0].Ports[0].HostPort = 1
|
||||||
if ok := DefaultPredicate(task, offer); ok {
|
if ok := DefaultPredicate(task, offer, nil); ok {
|
||||||
t.Fatalf("accepted offer %v:", offer)
|
t.Fatalf("accepted offer %v:", offer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,21 +270,60 @@ func TestGeneratePodName(t *testing.T) {
|
|||||||
func TestNodeSelector(t *testing.T) {
|
func TestNodeSelector(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
sel1 := map[string]string{"rack": "a"}
|
newNode := func(hostName string, l map[string]string) *api.Node {
|
||||||
sel2 := map[string]string{"rack": "a", "gen": "2014"}
|
nodeLabels := map[string]string{"kubernetes.io/hostname": hostName}
|
||||||
|
if l != nil {
|
||||||
|
for k, v := range l {
|
||||||
|
nodeLabels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &api.Node{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: hostName,
|
||||||
|
Labels: nodeLabels,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: hostName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node1 := newNode("node1", node.SlaveAttributesToLabels([]*mesos.Attribute{
|
||||||
|
newTextAttribute("rack", "a"),
|
||||||
|
newTextAttribute("gen", "2014"),
|
||||||
|
newScalarAttribute("num", 42.0),
|
||||||
|
}))
|
||||||
|
node2 := newNode("node2", node.SlaveAttributesToLabels([]*mesos.Attribute{
|
||||||
|
newTextAttribute("rack", "b"),
|
||||||
|
newTextAttribute("gen", "2015"),
|
||||||
|
newScalarAttribute("num", 0.0),
|
||||||
|
}))
|
||||||
|
labels3 := node.SlaveAttributesToLabels([]*mesos.Attribute{
|
||||||
|
newTextAttribute("rack", "c"),
|
||||||
|
newTextAttribute("gen", "2015"),
|
||||||
|
newScalarAttribute("old", 42),
|
||||||
|
})
|
||||||
|
labels3["some.other/label"] = "43"
|
||||||
|
node3 := newNode("node3", labels3)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
selector map[string]string
|
selector map[string]string
|
||||||
attrs []*mesos.Attribute
|
node *api.Node
|
||||||
ok bool
|
ok bool
|
||||||
|
desc string
|
||||||
}{
|
}{
|
||||||
{sel1, []*mesos.Attribute{newTextAttribute("rack", "a")}, true},
|
{map[string]string{"k8s.mesosphere.io/attribute-rack": "a"}, node1, true, "label value matches"},
|
||||||
{sel1, []*mesos.Attribute{newTextAttribute("rack", "b")}, false},
|
{map[string]string{"k8s.mesosphere.io/attribute-rack": "b"}, node1, false, "label value does not match"},
|
||||||
{sel1, []*mesos.Attribute{newTextAttribute("rack", "a"), newTextAttribute("gen", "2014")}, true},
|
{map[string]string{"k8s.mesosphere.io/attribute-rack": "a", "k8s.mesosphere.io/attribute-gen": "2014"}, node1, true, "multiple required labels match"},
|
||||||
{sel1, []*mesos.Attribute{newTextAttribute("rack", "a"), newScalarAttribute("num", 42.0)}, true},
|
{map[string]string{"k8s.mesosphere.io/attribute-rack": "a", "k8s.mesosphere.io/attribute-gen": "2015"}, node1, false, "one label does not match"},
|
||||||
{sel1, []*mesos.Attribute{newScalarAttribute("rack", 42.0)}, false},
|
{map[string]string{"k8s.mesosphere.io/attribute-rack": "a", "k8s.mesosphere.io/attribute-num": "42"}, node1, true, "scalar label matches"},
|
||||||
{sel2, []*mesos.Attribute{newTextAttribute("rack", "a"), newTextAttribute("gen", "2014")}, true},
|
{map[string]string{"k8s.mesosphere.io/attribute-rack": "a", "k8s.mesosphere.io/attribute-num": "43"}, node1, false, "scalar label does not match"},
|
||||||
{sel2, []*mesos.Attribute{newTextAttribute("rack", "a"), newTextAttribute("gen", "2015")}, false},
|
|
||||||
|
{map[string]string{"kubernetes.io/hostname": "node1"}, node1, true, "hostname label matches"},
|
||||||
|
{map[string]string{"kubernetes.io/hostname": "node2"}, node1, false, "hostname label does not match"},
|
||||||
|
{map[string]string{"kubernetes.io/hostname": "node2"}, node2, true, "hostname label matches"},
|
||||||
|
|
||||||
|
{map[string]string{"some.other/label": "43"}, node1, false, "non-slave attribute does not match"},
|
||||||
|
{map[string]string{"some.other/label": "43"}, node3, true, "non-slave attribute matches"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ts := range tests {
|
for _, ts := range tests {
|
||||||
@ -295,10 +334,10 @@ func TestNodeSelector(t *testing.T) {
|
|||||||
mutil.NewScalarResource("cpus", t_min_cpu),
|
mutil.NewScalarResource("cpus", t_min_cpu),
|
||||||
mutil.NewScalarResource("mem", t_min_mem),
|
mutil.NewScalarResource("mem", t_min_mem),
|
||||||
},
|
},
|
||||||
Attributes: ts.attrs,
|
Hostname: &ts.node.Name,
|
||||||
}
|
}
|
||||||
if got, want := DefaultPredicate(task, offer), ts.ok; got != want {
|
if got, want := DefaultPredicate(task, offer, ts.node), ts.ok; got != want {
|
||||||
t.Fatalf("expected acceptance of offer %v for selector %v to be %v, got %v:", want, got, ts.attrs, ts.selector)
|
t.Fatalf("expected acceptance of offer for selector %v to be %v, got %v: %q", ts.selector, want, got, ts.desc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
log "github.com/golang/glog"
|
log "github.com/golang/glog"
|
||||||
mesos "github.com/mesos/mesos-go/mesosproto"
|
mesos "github.com/mesos/mesos-go/mesosproto"
|
||||||
mresource "k8s.io/kubernetes/contrib/mesos/pkg/scheduler/resource"
|
mresource "k8s.io/kubernetes/contrib/mesos/pkg/scheduler/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/labels"
|
"k8s.io/kubernetes/pkg/labels"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,25 +32,25 @@ var DefaultPredicate = RequireAllPredicate([]FitPredicate{
|
|||||||
}).Fit
|
}).Fit
|
||||||
|
|
||||||
// FitPredicate implementations determine if the given task "fits" into offered Mesos resources.
|
// FitPredicate implementations determine if the given task "fits" into offered Mesos resources.
|
||||||
// Neither the task or offer should be modified.
|
// Neither the task or offer should be modified. Note that the node can be nil.
|
||||||
type FitPredicate func(*T, *mesos.Offer) bool
|
type FitPredicate func(*T, *mesos.Offer, *api.Node) bool
|
||||||
|
|
||||||
type RequireAllPredicate []FitPredicate
|
type RequireAllPredicate []FitPredicate
|
||||||
|
|
||||||
func (f RequireAllPredicate) Fit(t *T, offer *mesos.Offer) bool {
|
func (f RequireAllPredicate) Fit(t *T, offer *mesos.Offer, n *api.Node) bool {
|
||||||
for _, p := range f {
|
for _, p := range f {
|
||||||
if !p(t, offer) {
|
if !p(t, offer, n) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidationPredicate(t *T, offer *mesos.Offer) bool {
|
func ValidationPredicate(t *T, offer *mesos.Offer, _ *api.Node) bool {
|
||||||
return t != nil && offer != nil
|
return t != nil && offer != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NodeSelectorPredicate(t *T, offer *mesos.Offer) bool {
|
func NodeSelectorPredicate(t *T, offer *mesos.Offer, n *api.Node) bool {
|
||||||
// if the user has specified a target host, make sure this offer is for that host
|
// if the user has specified a target host, make sure this offer is for that host
|
||||||
if t.Pod.Spec.NodeName != "" && offer.GetHostname() != t.Pod.Spec.NodeName {
|
if t.Pod.Spec.NodeName != "" && offer.GetHostname() != t.Pod.Spec.NodeName {
|
||||||
return false
|
return false
|
||||||
@ -57,21 +58,18 @@ func NodeSelectorPredicate(t *T, offer *mesos.Offer) bool {
|
|||||||
|
|
||||||
// check the NodeSelector
|
// check the NodeSelector
|
||||||
if len(t.Pod.Spec.NodeSelector) > 0 {
|
if len(t.Pod.Spec.NodeSelector) > 0 {
|
||||||
slaveLabels := map[string]string{}
|
if n.Labels == nil {
|
||||||
for _, a := range offer.Attributes {
|
return false
|
||||||
if a.GetType() == mesos.Value_TEXT {
|
|
||||||
slaveLabels[a.GetName()] = a.GetText().GetValue()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
selector := labels.SelectorFromSet(t.Pod.Spec.NodeSelector)
|
selector := labels.SelectorFromSet(t.Pod.Spec.NodeSelector)
|
||||||
if !selector.Matches(labels.Set(slaveLabels)) {
|
if !selector.Matches(labels.Set(n.Labels)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func PortsPredicate(t *T, offer *mesos.Offer) bool {
|
func PortsPredicate(t *T, offer *mesos.Offer, _ *api.Node) bool {
|
||||||
// check ports
|
// check ports
|
||||||
if _, err := t.mapper.Generate(t, offer); err != nil {
|
if _, err := t.mapper.Generate(t, offer); err != nil {
|
||||||
log.V(3).Info(err)
|
log.V(3).Info(err)
|
||||||
@ -80,7 +78,7 @@ func PortsPredicate(t *T, offer *mesos.Offer) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func PodFitsResourcesPredicate(t *T, offer *mesos.Offer) bool {
|
func PodFitsResourcesPredicate(t *T, offer *mesos.Offer, _ *api.Node) bool {
|
||||||
// find offered cpu and mem
|
// find offered cpu and mem
|
||||||
var (
|
var (
|
||||||
offeredCpus mresource.CPUShares
|
offeredCpus mresource.CPUShares
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
bindings "github.com/mesos/mesos-go/scheduler"
|
bindings "github.com/mesos/mesos-go/scheduler"
|
||||||
execcfg "k8s.io/kubernetes/contrib/mesos/pkg/executor/config"
|
execcfg "k8s.io/kubernetes/contrib/mesos/pkg/executor/config"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/executor/messages"
|
"k8s.io/kubernetes/contrib/mesos/pkg/executor/messages"
|
||||||
|
"k8s.io/kubernetes/contrib/mesos/pkg/node"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/offers"
|
"k8s.io/kubernetes/contrib/mesos/pkg/offers"
|
||||||
offerMetrics "k8s.io/kubernetes/contrib/mesos/pkg/offers/metrics"
|
offerMetrics "k8s.io/kubernetes/contrib/mesos/pkg/offers/metrics"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/proc"
|
"k8s.io/kubernetes/contrib/mesos/pkg/proc"
|
||||||
@ -82,6 +83,7 @@ type KubernetesScheduler struct {
|
|||||||
etcdClient tools.EtcdClient
|
etcdClient tools.EtcdClient
|
||||||
failoverTimeout float64 // in seconds
|
failoverTimeout float64 // in seconds
|
||||||
reconcileInterval int64
|
reconcileInterval int64
|
||||||
|
nodeRegistrator node.Registrator
|
||||||
|
|
||||||
// Mesos context.
|
// Mesos context.
|
||||||
|
|
||||||
@ -116,6 +118,7 @@ type Config struct {
|
|||||||
FailoverTimeout float64
|
FailoverTimeout float64
|
||||||
ReconcileInterval int64
|
ReconcileInterval int64
|
||||||
ReconcileCooldown time.Duration
|
ReconcileCooldown time.Duration
|
||||||
|
LookupNode node.LookupFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new KubernetesScheduler
|
// New creates a new KubernetesScheduler
|
||||||
@ -131,16 +134,23 @@ func New(config Config) *KubernetesScheduler {
|
|||||||
etcdClient: config.EtcdClient,
|
etcdClient: config.EtcdClient,
|
||||||
failoverTimeout: config.FailoverTimeout,
|
failoverTimeout: config.FailoverTimeout,
|
||||||
reconcileInterval: config.ReconcileInterval,
|
reconcileInterval: config.ReconcileInterval,
|
||||||
|
nodeRegistrator: node.NewRegistrator(config.Client, config.LookupNode),
|
||||||
offers: offers.CreateRegistry(offers.RegistryConfig{
|
offers: offers.CreateRegistry(offers.RegistryConfig{
|
||||||
Compat: func(o *mesos.Offer) bool {
|
Compat: func(o *mesos.Offer) bool {
|
||||||
// filter the offers: the executor IDs must not identify a kubelet-
|
// the node must be registered and have up-to-date labels
|
||||||
// executor with a group that doesn't match ours
|
n := config.LookupNode(o.GetHostname())
|
||||||
|
if n == nil || !node.IsUpToDate(n, node.SlaveAttributesToLabels(o.GetAttributes())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// the executor IDs must not identify a kubelet-executor with a group that doesn't match ours
|
||||||
for _, eid := range o.GetExecutorIds() {
|
for _, eid := range o.GetExecutorIds() {
|
||||||
execuid := uid.Parse(eid.GetValue())
|
execuid := uid.Parse(eid.GetValue())
|
||||||
if execuid.Name() == execcfg.DefaultInfoID && execuid.Group() != k.executorGroup {
|
if execuid.Name() == execcfg.DefaultInfoID && execuid.Group() != k.executorGroup {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
DeclineOffer: func(id string) <-chan error {
|
DeclineOffer: func(id string) <-chan error {
|
||||||
@ -183,6 +193,7 @@ func (k *KubernetesScheduler) Init(electedMaster proc.Process, pl PluginInterfac
|
|||||||
k.plugin = pl
|
k.plugin = pl
|
||||||
k.offers.Init(k.terminate)
|
k.offers.Init(k.terminate)
|
||||||
k.InstallDebugHandlers(mux)
|
k.InstallDebugHandlers(mux)
|
||||||
|
k.nodeRegistrator.Run(k.terminate)
|
||||||
return k.recoverTasks()
|
return k.recoverTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,6 +334,15 @@ func (k *KubernetesScheduler) ResourceOffers(driver bindings.SchedulerDriver, of
|
|||||||
for _, offer := range offers {
|
for _, offer := range offers {
|
||||||
slaveId := offer.GetSlaveId().GetValue()
|
slaveId := offer.GetSlaveId().GetValue()
|
||||||
k.slaveHostNames.Register(slaveId, offer.GetHostname())
|
k.slaveHostNames.Register(slaveId, offer.GetHostname())
|
||||||
|
|
||||||
|
// create api object if not existing already
|
||||||
|
if k.nodeRegistrator != nil {
|
||||||
|
labels := node.SlaveAttributesToLabels(offer.GetAttributes())
|
||||||
|
_, err := k.nodeRegistrator.Register(offer.GetHostname(), labels)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package scheduler
|
package scheduler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mesos "github.com/mesos/mesos-go/mesosproto"
|
mesos "github.com/mesos/mesos-go/mesosproto"
|
||||||
@ -27,6 +28,8 @@ import (
|
|||||||
schedcfg "k8s.io/kubernetes/contrib/mesos/pkg/scheduler/config"
|
schedcfg "k8s.io/kubernetes/contrib/mesos/pkg/scheduler/config"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/podtask"
|
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/podtask"
|
||||||
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/slave"
|
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/slave"
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/client/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
//get number of non-expired offers from offer registry
|
//get number of non-expired offers from offer registry
|
||||||
@ -42,10 +45,47 @@ func getNumberOffers(os offers.Registry) int {
|
|||||||
return walked
|
return walked
|
||||||
}
|
}
|
||||||
|
|
||||||
//test adding of ressource offer, should be added to offer registry and slavesf
|
type mockRegistrator struct {
|
||||||
|
store cache.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRegistrator) Run(terminate <-chan struct{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRegistrator) Register(hostName string, labels map[string]string) (bool, error) {
|
||||||
|
obj, _, err := r.store.GetByKey(hostName)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if obj == nil {
|
||||||
|
return true, r.store.Add(&api.Node{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: hostName,
|
||||||
|
Labels: labels,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: hostName,
|
||||||
|
},
|
||||||
|
Status: api.NodeStatus{
|
||||||
|
Phase: api.NodePending,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
n := obj.(*api.Node)
|
||||||
|
if reflect.DeepEqual(n.Labels, labels) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
n.Labels = labels
|
||||||
|
return true, r.store.Update(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//test adding of ressource offer, should be added to offer registry and slaves
|
||||||
func TestResourceOffer_Add(t *testing.T) {
|
func TestResourceOffer_Add(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
registrator := &mockRegistrator{cache.NewStore(cache.MetaNamespaceKeyFunc)}
|
||||||
testScheduler := &KubernetesScheduler{
|
testScheduler := &KubernetesScheduler{
|
||||||
offers: offers.CreateRegistry(offers.RegistryConfig{
|
offers: offers.CreateRegistry(offers.RegistryConfig{
|
||||||
Compat: func(o *mesos.Offer) bool {
|
Compat: func(o *mesos.Offer) bool {
|
||||||
@ -60,6 +100,7 @@ func TestResourceOffer_Add(t *testing.T) {
|
|||||||
ListenerDelay: schedcfg.DefaultListenerDelay,
|
ListenerDelay: schedcfg.DefaultListenerDelay,
|
||||||
}),
|
}),
|
||||||
slaveHostNames: slave.NewRegistry(),
|
slaveHostNames: slave.NewRegistry(),
|
||||||
|
nodeRegistrator: registrator,
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname := "h1"
|
hostname := "h1"
|
||||||
@ -67,6 +108,7 @@ func TestResourceOffer_Add(t *testing.T) {
|
|||||||
offer1 := &mesos.Offer{Id: offerID1, Hostname: &hostname, SlaveId: util.NewSlaveID(hostname)}
|
offer1 := &mesos.Offer{Id: offerID1, Hostname: &hostname, SlaveId: util.NewSlaveID(hostname)}
|
||||||
offers1 := []*mesos.Offer{offer1}
|
offers1 := []*mesos.Offer{offer1}
|
||||||
testScheduler.ResourceOffers(nil, offers1)
|
testScheduler.ResourceOffers(nil, offers1)
|
||||||
|
assert.Equal(1, len(registrator.store.List()))
|
||||||
|
|
||||||
assert.Equal(1, getNumberOffers(testScheduler.offers))
|
assert.Equal(1, getNumberOffers(testScheduler.offers))
|
||||||
//check slave hostname
|
//check slave hostname
|
||||||
|
@ -63,8 +63,10 @@ import (
|
|||||||
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/uid"
|
"k8s.io/kubernetes/contrib/mesos/pkg/scheduler/uid"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/api/resource"
|
"k8s.io/kubernetes/pkg/api/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/client/cache"
|
||||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||||
clientauth "k8s.io/kubernetes/pkg/client/unversioned/auth"
|
clientauth "k8s.io/kubernetes/pkg/client/unversioned/auth"
|
||||||
|
"k8s.io/kubernetes/pkg/fields"
|
||||||
"k8s.io/kubernetes/pkg/healthz"
|
"k8s.io/kubernetes/pkg/healthz"
|
||||||
"k8s.io/kubernetes/pkg/master/ports"
|
"k8s.io/kubernetes/pkg/master/ports"
|
||||||
etcdstorage "k8s.io/kubernetes/pkg/storage/etcd"
|
etcdstorage "k8s.io/kubernetes/pkg/storage/etcd"
|
||||||
@ -76,6 +78,7 @@ const (
|
|||||||
defaultMesosUser = "root" // should have privs to execute docker and iptables commands
|
defaultMesosUser = "root" // should have privs to execute docker and iptables commands
|
||||||
defaultReconcileInterval = 300 // 5m default task reconciliation interval
|
defaultReconcileInterval = 300 // 5m default task reconciliation interval
|
||||||
defaultReconcileCooldown = 15 * time.Second
|
defaultReconcileCooldown = 15 * time.Second
|
||||||
|
defaultNodeRelistPeriod = 5 * time.Minute
|
||||||
defaultFrameworkName = "Kubernetes"
|
defaultFrameworkName = "Kubernetes"
|
||||||
defaultExecutorCPUs = mresource.CPUShares(0.25) // initial CPU allocated for executor
|
defaultExecutorCPUs = mresource.CPUShares(0.25) // initial CPU allocated for executor
|
||||||
defaultExecutorMem = mresource.MegaBytes(128.0) // initial memory allocated for executor
|
defaultExecutorMem = mresource.MegaBytes(128.0) // initial memory allocated for executor
|
||||||
@ -145,6 +148,7 @@ type SchedulerServer struct {
|
|||||||
DockerCfgPath string
|
DockerCfgPath string
|
||||||
ContainPodResources bool
|
ContainPodResources bool
|
||||||
AccountForPodResources bool
|
AccountForPodResources bool
|
||||||
|
nodeRelistPeriod time.Duration
|
||||||
|
|
||||||
executable string // path to the binary running this service
|
executable string // path to the binary running this service
|
||||||
client *client.Client
|
client *client.Client
|
||||||
@ -192,6 +196,7 @@ func NewSchedulerServer() *SchedulerServer {
|
|||||||
KubeletSyncFrequency: 10 * time.Second,
|
KubeletSyncFrequency: 10 * time.Second,
|
||||||
ContainPodResources: true,
|
ContainPodResources: true,
|
||||||
AccountForPodResources: true,
|
AccountForPodResources: true,
|
||||||
|
nodeRelistPeriod: defaultNodeRelistPeriod,
|
||||||
}
|
}
|
||||||
// cache this for later use. also useful in case the original binary gets deleted, e.g.
|
// cache this for later use. also useful in case the original binary gets deleted, e.g.
|
||||||
// during upgrades, development deployments, etc.
|
// during upgrades, development deployments, etc.
|
||||||
@ -245,6 +250,7 @@ func (s *SchedulerServer) addCoreFlags(fs *pflag.FlagSet) {
|
|||||||
fs.Var(&s.DefaultContainerMemLimit, "default-container-mem-limit", "Containers without a memory resource limit are admitted this much amount of memory in MB")
|
fs.Var(&s.DefaultContainerMemLimit, "default-container-mem-limit", "Containers without a memory resource limit are admitted this much amount of memory in MB")
|
||||||
fs.BoolVar(&s.ContainPodResources, "contain-pod-resources", s.ContainPodResources, "Reparent pod containers into mesos cgroups; disable if you're having strange mesos/docker/systemd interactions.")
|
fs.BoolVar(&s.ContainPodResources, "contain-pod-resources", s.ContainPodResources, "Reparent pod containers into mesos cgroups; disable if you're having strange mesos/docker/systemd interactions.")
|
||||||
fs.BoolVar(&s.AccountForPodResources, "account-for-pod-resources", s.AccountForPodResources, "Allocate pod CPU and memory resources from offers (Default: true)")
|
fs.BoolVar(&s.AccountForPodResources, "account-for-pod-resources", s.AccountForPodResources, "Allocate pod CPU and memory resources from offers (Default: true)")
|
||||||
|
fs.DurationVar(&s.nodeRelistPeriod, "node-monitor-period", s.nodeRelistPeriod, "Period between relisting of all nodes from the apiserver.")
|
||||||
|
|
||||||
fs.IntVar(&s.ExecutorLogV, "executor-logv", s.ExecutorLogV, "Logging verbosity of spawned minion and executor processes.")
|
fs.IntVar(&s.ExecutorLogV, "executor-logv", s.ExecutorLogV, "Logging verbosity of spawned minion and executor processes.")
|
||||||
fs.BoolVar(&s.ExecutorBindall, "executor-bindall", s.ExecutorBindall, "When true will set -address of the executor to 0.0.0.0.")
|
fs.BoolVar(&s.ExecutorBindall, "executor-bindall", s.ExecutorBindall, "When true will set -address of the executor to 0.0.0.0.")
|
||||||
@ -678,7 +684,24 @@ func (s *SchedulerServer) bootstrap(hks hyperkube.Interface, sc *schedcfg.Config
|
|||||||
podtask.DefaultMinimalProcurement)
|
podtask.DefaultMinimalProcurement)
|
||||||
}
|
}
|
||||||
|
|
||||||
fcfs := scheduler.NewFCFSPodScheduler(as)
|
// mirror all nodes into the nodeStore
|
||||||
|
nodesClient, err := s.createAPIServerClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Cannot create client to watch nodes: %v", err)
|
||||||
|
}
|
||||||
|
nodeStore := cache.NewStore(cache.MetaNamespaceKeyFunc)
|
||||||
|
nodeLW := cache.NewListWatchFromClient(nodesClient, "nodes", api.NamespaceAll, fields.Everything())
|
||||||
|
cache.NewReflector(nodeLW, &api.Node{}, nodeStore, s.nodeRelistPeriod).Run()
|
||||||
|
|
||||||
|
lookupNode := func(hostName string) *api.Node {
|
||||||
|
n, _, _ := nodeStore.GetByKey(hostName) // ignore error and return nil then
|
||||||
|
if n == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return n.(*api.Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
fcfs := scheduler.NewFCFSPodScheduler(as, lookupNode)
|
||||||
mesosPodScheduler := scheduler.New(scheduler.Config{
|
mesosPodScheduler := scheduler.New(scheduler.Config{
|
||||||
Schedcfg: *sc,
|
Schedcfg: *sc,
|
||||||
Executor: executor,
|
Executor: executor,
|
||||||
@ -688,6 +711,7 @@ func (s *SchedulerServer) bootstrap(hks hyperkube.Interface, sc *schedcfg.Config
|
|||||||
FailoverTimeout: s.FailoverTimeout,
|
FailoverTimeout: s.FailoverTimeout,
|
||||||
ReconcileInterval: s.ReconcileInterval,
|
ReconcileInterval: s.ReconcileInterval,
|
||||||
ReconcileCooldown: s.ReconcileCooldown,
|
ReconcileCooldown: s.ReconcileCooldown,
|
||||||
|
LookupNode: lookupNode,
|
||||||
})
|
})
|
||||||
|
|
||||||
masterUri := s.MesosMaster
|
masterUri := s.MesosMaster
|
||||||
|
53
test/e2e/mesos.go
Normal file
53
test/e2e/mesos.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/labels"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"k8s.io/kubernetes/pkg/fields"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Mesos", func() {
|
||||||
|
framework := NewFramework("pods")
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
SkipUnlessProviderIs("mesos/docker")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("applies slave attributes as labels", func() {
|
||||||
|
nodeClient := framework.Client.Nodes()
|
||||||
|
|
||||||
|
rackA := labels.SelectorFromSet(map[string]string{"k8s.mesosphere.io/attribute-rack": "1"})
|
||||||
|
nodes, err := nodeClient.List(rackA, fields.Everything())
|
||||||
|
if err != nil {
|
||||||
|
Failf("Failed to query for node: %v", err)
|
||||||
|
}
|
||||||
|
Expect(len(nodes.Items)).To(Equal(1))
|
||||||
|
|
||||||
|
var addr string
|
||||||
|
for _, a := range nodes.Items[0].Status.Addresses {
|
||||||
|
if a.Type == api.NodeInternalIP {
|
||||||
|
addr = a.Address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expect(len(addr)).NotTo(Equal(""))
|
||||||
|
})
|
||||||
|
})
|
@ -334,7 +334,7 @@ var _ = Describe("SchedulerPredicates", func() {
|
|||||||
|
|
||||||
// Test Nodes does not have any label, hence it should be impossible to schedule Pod with
|
// Test Nodes does not have any label, hence it should be impossible to schedule Pod with
|
||||||
// nonempty Selector set.
|
// nonempty Selector set.
|
||||||
It("validates that NodeSelector is respected.", func() {
|
It("validates that NodeSelector is respected if not matching", func() {
|
||||||
By("Trying to schedule Pod with nonempty NodeSelector.")
|
By("Trying to schedule Pod with nonempty NodeSelector.")
|
||||||
podName := "restricted-pod"
|
podName := "restricted-pod"
|
||||||
|
|
||||||
@ -371,4 +371,77 @@ var _ = Describe("SchedulerPredicates", func() {
|
|||||||
verifyResult(c, podName, ns, currentlyDeadPods)
|
verifyResult(c, podName, ns, currentlyDeadPods)
|
||||||
cleanupPods(c, ns)
|
cleanupPods(c, ns)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("validates that NodeSelector is respected if matching.", func() {
|
||||||
|
// launch a pod to find a node which can launch a pod. We intentionally do
|
||||||
|
// not just take the node list and choose the first of them. Depending on the
|
||||||
|
// cluster and the scheduler it might be that a "normal" pod cannot be
|
||||||
|
// scheduled onto it.
|
||||||
|
By("Trying to launch a pod without a label to get a node which can launch it.")
|
||||||
|
podName := "without-label"
|
||||||
|
_, err := c.Pods(ns).Create(&api.Pod{
|
||||||
|
TypeMeta: unversioned.TypeMeta{
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: podName,
|
||||||
|
},
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Name: podName,
|
||||||
|
Image: "gcr.io/google_containers/pause:go",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expectNoError(err)
|
||||||
|
expectNoError(waitForPodRunningInNamespace(c, podName, ns))
|
||||||
|
pod, err := c.Pods(ns).Get(podName)
|
||||||
|
expectNoError(err)
|
||||||
|
|
||||||
|
nodeName := pod.Spec.NodeName
|
||||||
|
err = c.Pods(ns).Delete(podName, api.NewDeleteOptions(0))
|
||||||
|
expectNoError(err)
|
||||||
|
|
||||||
|
By("Trying to apply a random label on the found node.")
|
||||||
|
k := fmt.Sprintf("kubernetes.io/e2e-%s", string(util.NewUUID()))
|
||||||
|
v := "42"
|
||||||
|
patch := fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s"}}}`, k, v)
|
||||||
|
err = c.Patch(api.MergePatchType).Resource("nodes").Name(nodeName).Body([]byte(patch)).Do().Error()
|
||||||
|
expectNoError(err)
|
||||||
|
|
||||||
|
node, err := c.Nodes().Get(nodeName)
|
||||||
|
expectNoError(err)
|
||||||
|
Expect(node.Labels[k]).To(Equal(v))
|
||||||
|
|
||||||
|
By("Trying to relaunch the pod, now with labels.")
|
||||||
|
labelPodName := "with-labels"
|
||||||
|
_, err = c.Pods(ns).Create(&api.Pod{
|
||||||
|
TypeMeta: unversioned.TypeMeta{
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: labelPodName,
|
||||||
|
},
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Name: labelPodName,
|
||||||
|
Image: "gcr.io/google_containers/pause:go",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NodeSelector: map[string]string{
|
||||||
|
"kubernetes.io/hostname": nodeName,
|
||||||
|
k: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expectNoError(err)
|
||||||
|
defer c.Pods(ns).Delete(labelPodName, api.NewDeleteOptions(0))
|
||||||
|
expectNoError(waitForPodRunningInNamespace(c, labelPodName, ns))
|
||||||
|
labelPod, err := c.Pods(ns).Get(labelPodName)
|
||||||
|
expectNoError(err)
|
||||||
|
Expect(labelPod.Spec.NodeName).To(Equal(nodeName))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user