mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 20:24:09 +00:00
commit
4384772d32
@ -104,6 +104,7 @@ type EnvVar struct {
|
|||||||
Value string `yaml:"value,omitempty" json:"value,omitempty"`
|
Value string `yaml:"value,omitempty" json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPGetProbe describes a liveness probe based on HTTP Get requests.
|
||||||
type HTTPGetProbe struct {
|
type HTTPGetProbe struct {
|
||||||
// Path to access on the http server
|
// Path to access on the http server
|
||||||
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
Path string `yaml:"path,omitempty" json:"path,omitempty"`
|
||||||
@ -113,6 +114,7 @@ type HTTPGetProbe struct {
|
|||||||
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LivenessProbe describes a liveness probe to be examined to the container.
|
||||||
type LivenessProbe struct {
|
type LivenessProbe struct {
|
||||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||||
// Type of liveness probe. Current legal values "http"
|
// Type of liveness probe. Current legal values "http"
|
||||||
|
@ -64,7 +64,7 @@ func makeNotFoundError(field string, value interface{}) ValidationError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A helper for accumulating errors. This could be moved to util if anyone else needs it.
|
// A helper for accumulating errors. This could be moved to util if anyone else needs it.
|
||||||
type errorList []error;
|
type errorList []error
|
||||||
|
|
||||||
func (list *errorList) Append(errs ...error) {
|
func (list *errorList) Append(errs ...error) {
|
||||||
*list = append(*list, errs...)
|
*list = append(*list, errs...)
|
||||||
@ -87,7 +87,7 @@ func validateVolumes(volumes []Volume) (util.StringSet, errorList) {
|
|||||||
return allNames, allErrs
|
return allNames, allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedPortProtocols util.StringSet = util.NewStringSet("TCP", "UDP")
|
var supportedPortProtocols = util.NewStringSet("TCP", "UDP")
|
||||||
|
|
||||||
func validatePorts(ports []Port) errorList {
|
func validatePorts(ports []Port) errorList {
|
||||||
allErrs := errorList{}
|
allErrs := errorList{}
|
||||||
@ -219,10 +219,10 @@ func validateContainers(containers []Container, volumes util.StringSet) errorLis
|
|||||||
// every Port.HostPort across the whole pod must be unique.
|
// every Port.HostPort across the whole pod must be unique.
|
||||||
allErrs.Append(checkHostPortConflicts(containers)...)
|
allErrs.Append(checkHostPortConflicts(containers)...)
|
||||||
|
|
||||||
return allErrs;
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedManifestVersions util.StringSet = util.NewStringSet("v1beta1", "v1beta2")
|
var supportedManifestVersions = util.NewStringSet("v1beta1", "v1beta2")
|
||||||
|
|
||||||
// ValidateManifest tests that the specified ContainerManifest has valid data.
|
// ValidateManifest tests that the specified ContainerManifest has valid data.
|
||||||
// This includes checking formatting and uniqueness. It also canonicalizes the
|
// This includes checking formatting and uniqueness. It also canonicalizes the
|
||||||
|
@ -20,7 +20,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CloudInterface is an abstract, pluggable interface for cloud providers
|
// Interface is an abstract, pluggable interface for cloud providers
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
// TCPLoadBalancer returns a balancer interface. Also returns true if the interface is supported, false otherwise.
|
// TCPLoadBalancer returns a balancer interface. Also returns true if the interface is supported, false otherwise.
|
||||||
TCPLoadBalancer() (TCPLoadBalancer, bool)
|
TCPLoadBalancer() (TCPLoadBalancer, bool)
|
||||||
@ -28,16 +28,23 @@ type Interface interface {
|
|||||||
Instances() (Instances, bool)
|
Instances() (Instances, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TCPLoadBalancer is an abstract, pluggable interface for TCP load balancers.
|
||||||
type TCPLoadBalancer interface {
|
type TCPLoadBalancer interface {
|
||||||
|
// TCPLoadBalancerExists returns whether the specified load balancer exists.
|
||||||
// TODO: Break this up into different interfaces (LB, etc) when we have more than one type of service
|
// TODO: Break this up into different interfaces (LB, etc) when we have more than one type of service
|
||||||
TCPLoadBalancerExists(name, region string) (bool, error)
|
TCPLoadBalancerExists(name, region string) (bool, error)
|
||||||
|
// CreateTCPLoadBalancer creates a new tcp load balancer.
|
||||||
CreateTCPLoadBalancer(name, region string, port int, hosts []string) error
|
CreateTCPLoadBalancer(name, region string, port int, hosts []string) error
|
||||||
|
// UpdateTCPLoadBalancer updates hosts under the specified load balancer.
|
||||||
UpdateTCPLoadBalancer(name, region string, hosts []string) error
|
UpdateTCPLoadBalancer(name, region string, hosts []string) error
|
||||||
|
// DeleteTCPLoadBalancer deletes a specified load balancer.
|
||||||
DeleteTCPLoadBalancer(name, region string) error
|
DeleteTCPLoadBalancer(name, region string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instances is an abstract, pluggable interface for sets of instances.
|
||||||
type Instances interface {
|
type Instances interface {
|
||||||
|
// IPAddress returns an IP address of the specified instance.
|
||||||
IPAddress(name string) (net.IP, error)
|
IPAddress(name string) (net.IP, error)
|
||||||
// Lists instances that match 'filter' which is a regular expression which must match the entire instance name (fqdn)
|
// List lists instances that match 'filter' which is a regular expression which must match the entire instance name (fqdn)
|
||||||
List(filter string) ([]string, error)
|
List(filter string) ([]string, error)
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FakeCloud is a test-double implementation of Interface, TCPLoadBalancer and Instances. It is useful for testing.
|
||||||
type FakeCloud struct {
|
type FakeCloud struct {
|
||||||
Exists bool
|
Exists bool
|
||||||
Err error
|
Err error
|
||||||
@ -33,42 +34,60 @@ func (f *FakeCloud) addCall(desc string) {
|
|||||||
f.Calls = append(f.Calls, desc)
|
f.Calls = append(f.Calls, desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearCalls clears internal record of method calls to this FakeCloud.
|
||||||
func (f *FakeCloud) ClearCalls() {
|
func (f *FakeCloud) ClearCalls() {
|
||||||
f.Calls = []string{}
|
f.Calls = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TCPLoadBalancer returns a fake implementation of TCPLoadBalancer.
|
||||||
|
//
|
||||||
|
// Actually it just returns f itself.
|
||||||
func (f *FakeCloud) TCPLoadBalancer() (TCPLoadBalancer, bool) {
|
func (f *FakeCloud) TCPLoadBalancer() (TCPLoadBalancer, bool) {
|
||||||
return f, true
|
return f, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instances returns a fake implementation of Instances.
|
||||||
|
//
|
||||||
|
// Actually it just returns f itself.
|
||||||
func (f *FakeCloud) Instances() (Instances, bool) {
|
func (f *FakeCloud) Instances() (Instances, bool) {
|
||||||
return f, true
|
return f, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TCPLoadBalancerExists is a stub implementation of TCPLoadBalancer.TCPLoadBalancerExists.
|
||||||
func (f *FakeCloud) TCPLoadBalancerExists(name, region string) (bool, error) {
|
func (f *FakeCloud) TCPLoadBalancerExists(name, region string) (bool, error) {
|
||||||
return f.Exists, f.Err
|
return f.Exists, f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.CreateTCPLoadBalancer.
|
||||||
|
// It adds an entry "create" into the internal method call record.
|
||||||
func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, port int, hosts []string) error {
|
func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, port int, hosts []string) error {
|
||||||
f.addCall("create")
|
f.addCall("create")
|
||||||
return f.Err
|
return f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.UpdateTCPLoadBalancer.
|
||||||
|
// It adds an entry "update" into the internal method call record.
|
||||||
func (f *FakeCloud) UpdateTCPLoadBalancer(name, region string, hosts []string) error {
|
func (f *FakeCloud) UpdateTCPLoadBalancer(name, region string, hosts []string) error {
|
||||||
f.addCall("update")
|
f.addCall("update")
|
||||||
return f.Err
|
return f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.DeleteTCPLoadBalancer.
|
||||||
|
// It adds an entry "delete" into the internal method call record.
|
||||||
func (f *FakeCloud) DeleteTCPLoadBalancer(name, region string) error {
|
func (f *FakeCloud) DeleteTCPLoadBalancer(name, region string) error {
|
||||||
f.addCall("delete")
|
f.addCall("delete")
|
||||||
return f.Err
|
return f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPAddress is a test-spy implementation of Instances.IPAddress.
|
||||||
|
// It adds an entry "ip-address" into the internal method call record.
|
||||||
func (f *FakeCloud) IPAddress(instance string) (net.IP, error) {
|
func (f *FakeCloud) IPAddress(instance string) (net.IP, error) {
|
||||||
f.addCall("ip-address")
|
f.addCall("ip-address")
|
||||||
return f.IP, f.Err
|
return f.IP, f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List is a test-spy implementation of Instances.List.
|
||||||
|
// It adds an entry "list" into the internal method call record.
|
||||||
func (f *FakeCloud) List(filter string) ([]string, error) {
|
func (f *FakeCloud) List(filter string) ([]string, error) {
|
||||||
f.addCall("list")
|
f.addCall("list")
|
||||||
result := []string{}
|
result := []string{}
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
compute "code.google.com/p/google-api-go-client/compute/v1"
|
compute "code.google.com/p/google-api-go-client/compute/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GCECloud is an implementation of Interface, TCPLoadBalancer and Instances for Google Compute Engine.
|
||||||
type GCECloud struct {
|
type GCECloud struct {
|
||||||
service *compute.Service
|
service *compute.Service
|
||||||
projectID string
|
projectID string
|
||||||
@ -61,6 +62,7 @@ func getProjectAndZone() (string, string, error) {
|
|||||||
return parts[1], parts[3], nil
|
return parts[1], parts[3], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewGCECloud creates a new instance of GCECloud.
|
||||||
func NewGCECloud() (*GCECloud, error) {
|
func NewGCECloud() (*GCECloud, error) {
|
||||||
projectID, zone, err := getProjectAndZone()
|
projectID, zone, err := getProjectAndZone()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -81,10 +83,12 @@ func NewGCECloud() (*GCECloud, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TCPLoadBalancer returns an implementation of TCPLoadBalancer for Google Compute Engine.
|
||||||
func (gce *GCECloud) TCPLoadBalancer() (TCPLoadBalancer, bool) {
|
func (gce *GCECloud) TCPLoadBalancer() (TCPLoadBalancer, bool) {
|
||||||
return gce, true
|
return gce, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instances returns an implementation of Instances for Google Compute Engine.
|
||||||
func (gce *GCECloud) Instances() (Instances, bool) {
|
func (gce *GCECloud) Instances() (Instances, bool) {
|
||||||
return gce, true
|
return gce, true
|
||||||
}
|
}
|
||||||
@ -128,11 +132,13 @@ func (gce *GCECloud) waitForRegionOp(op *compute.Operation, region string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TCPLoadBalancerExists is an implementation of TCPLoadBalancer.TCPLoadBalancerExists.
|
||||||
func (gce *GCECloud) TCPLoadBalancerExists(name, region string) (bool, error) {
|
func (gce *GCECloud) TCPLoadBalancerExists(name, region string) (bool, error) {
|
||||||
_, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do()
|
_, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do()
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer.
|
||||||
func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, port int, hosts []string) error {
|
func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, port int, hosts []string) error {
|
||||||
pool, err := gce.makeTargetPool(name, region, hosts)
|
pool, err := gce.makeTargetPool(name, region, hosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -148,6 +154,7 @@ func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, port int, hosts
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTCPLoadBalancer is an implementation of TCPLoadBalancer.UpdateTCPLoadBalancer.
|
||||||
func (gce *GCECloud) UpdateTCPLoadBalancer(name, region string, hosts []string) error {
|
func (gce *GCECloud) UpdateTCPLoadBalancer(name, region string, hosts []string) error {
|
||||||
var refs []*compute.InstanceReference
|
var refs []*compute.InstanceReference
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
@ -161,6 +168,7 @@ func (gce *GCECloud) UpdateTCPLoadBalancer(name, region string, hosts []string)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteTCPLoadBalancer is an implementation of TCPLoadBalancer.DeleteTCPLoadBalancer.
|
||||||
func (gce *GCECloud) DeleteTCPLoadBalancer(name, region string) error {
|
func (gce *GCECloud) DeleteTCPLoadBalancer(name, region string) error {
|
||||||
_, err := gce.service.ForwardingRules.Delete(gce.projectID, region, name).Do()
|
_, err := gce.service.ForwardingRules.Delete(gce.projectID, region, name).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -170,6 +178,7 @@ func (gce *GCECloud) DeleteTCPLoadBalancer(name, region string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPAddress is an implementation of Instances.IPAddress.
|
||||||
func (gce *GCECloud) IPAddress(instance string) (net.IP, error) {
|
func (gce *GCECloud) IPAddress(instance string) (net.IP, error) {
|
||||||
res, err := gce.service.Instances.Get(gce.projectID, gce.zone, instance).Do()
|
res, err := gce.service.Instances.Get(gce.projectID, gce.zone, instance).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -195,6 +204,7 @@ func fqdnSuffix() (string, error) {
|
|||||||
return strings.TrimSpace(string(fullHostname)[len(string(hostname)):]), nil
|
return strings.TrimSpace(string(fullHostname)[len(string(hostname)):]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List is an implementation of Instances.List.
|
||||||
func (gce *GCECloud) List(filter string) ([]string, error) {
|
func (gce *GCECloud) List(filter string) ([]string, error) {
|
||||||
// GCE gives names without their fqdn suffix, so get that here for appending.
|
// GCE gives names without their fqdn suffix, so get that here for appending.
|
||||||
// This is needed because the kubelet looks for its jobs in /registry/hosts/<fqdn>/pods
|
// This is needed because the kubelet looks for its jobs in /registry/hosts/<fqdn>/pods
|
||||||
|
@ -44,13 +44,16 @@ type ReplicationManager struct {
|
|||||||
syncHandler func(controllerSpec api.ReplicationController) error
|
syncHandler func(controllerSpec api.ReplicationController) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// An interface that knows how to add or delete pods
|
// PodControlInterface is an interface that knows how to add or delete pods
|
||||||
// created as an interface to allow testing.
|
// created as an interface to allow testing.
|
||||||
type PodControlInterface interface {
|
type PodControlInterface interface {
|
||||||
|
// createReplica creates new replicated pods according to the spec.
|
||||||
createReplica(controllerSpec api.ReplicationController)
|
createReplica(controllerSpec api.ReplicationController)
|
||||||
|
// deletePod deletes the pod identified by podID.
|
||||||
deletePod(podID string) error
|
deletePod(podID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RealPodControl is the default implementation of PodControllerInterface.
|
||||||
type RealPodControl struct {
|
type RealPodControl struct {
|
||||||
kubeClient client.ClientInterface
|
kubeClient client.ClientInterface
|
||||||
}
|
}
|
||||||
@ -77,6 +80,7 @@ func (r RealPodControl) deletePod(podID string) error {
|
|||||||
return r.kubeClient.DeletePod(podID)
|
return r.kubeClient.DeletePod(podID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeReplicationManager craetes a new ReplicationManager.
|
||||||
func MakeReplicationManager(etcdClient tools.EtcdClient, kubeClient client.ClientInterface) *ReplicationManager {
|
func MakeReplicationManager(etcdClient tools.EtcdClient, kubeClient client.ClientInterface) *ReplicationManager {
|
||||||
rm := &ReplicationManager{
|
rm := &ReplicationManager{
|
||||||
kubeClient: kubeClient,
|
kubeClient: kubeClient,
|
||||||
@ -91,7 +95,7 @@ func MakeReplicationManager(etcdClient tools.EtcdClient, kubeClient client.Clien
|
|||||||
return rm
|
return rm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin watching and syncing.
|
// Run begins watching and syncing.
|
||||||
func (rm *ReplicationManager) Run(period time.Duration) {
|
func (rm *ReplicationManager) Run(period time.Duration) {
|
||||||
rm.syncTime = time.Tick(period)
|
rm.syncTime = time.Tick(period)
|
||||||
go util.Forever(func() { rm.watchControllers() }, period)
|
go util.Forever(func() { rm.watchControllers() }, period)
|
||||||
@ -145,9 +149,8 @@ func (rm *ReplicationManager) handleWatchResponse(response *etcd.Response) (*api
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &controllerSpec, nil
|
return &controllerSpec, nil
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("response node is null %#v", response)
|
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("response node is null %#v", response)
|
||||||
} else if response.Action == "delete" {
|
} else if response.Action == "delete" {
|
||||||
// Ensure that the final state of a replication controller is applied before it is deleted.
|
// Ensure that the final state of a replication controller is applied before it is deleted.
|
||||||
// Otherwise, a replication controller could be modified and then deleted (for example, from 3 to 0
|
// Otherwise, a replication controller could be modified and then deleted (for example, from 3 to 0
|
||||||
@ -158,9 +161,8 @@ func (rm *ReplicationManager) handleWatchResponse(response *etcd.Response) (*api
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &controllerSpec, nil
|
return &controllerSpec, nil
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("previous node is null %#v", response)
|
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("previous node is null %#v", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -41,7 +41,7 @@ func expectNoError(t *testing.T, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUrl(suffix string) string {
|
func makeURL(suffix string) string {
|
||||||
return apiPath + suffix
|
return apiPath + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ func TestCreateReplica(t *testing.T) {
|
|||||||
// DesiredState: controllerSpec.DesiredState.PodTemplate.DesiredState,
|
// DesiredState: controllerSpec.DesiredState.PodTemplate.DesiredState,
|
||||||
//}
|
//}
|
||||||
// TODO: fix this so that it validates the body.
|
// TODO: fix this so that it validates the body.
|
||||||
fakeHandler.ValidateRequest(t, makeUrl("/pods"), "POST", nil)
|
fakeHandler.ValidateRequest(t, makeURL("/pods"), "POST", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleWatchResponseNotSet(t *testing.T) {
|
func TestHandleWatchResponseNotSet(t *testing.T) {
|
||||||
|
@ -39,7 +39,7 @@ func promptForString(field string) string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse an AuthInfo object from a file path. Prompt user and create file if it doesn't exist.
|
// LoadAuthInfo parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
|
||||||
func LoadAuthInfo(path string) (*client.AuthInfo, error) {
|
func LoadAuthInfo(path string) (*client.AuthInfo, error) {
|
||||||
var auth client.AuthInfo
|
var auth client.AuthInfo
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
@ -63,7 +63,7 @@ func LoadAuthInfo(path string) (*client.AuthInfo, error) {
|
|||||||
return &auth, err
|
return &auth, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform a rolling update of a collection of pods.
|
// Update performs a rolling update of a collection of pods.
|
||||||
// 'name' points to a replication controller.
|
// 'name' points to a replication controller.
|
||||||
// 'client' is used for updating pods.
|
// 'client' is used for updating pods.
|
||||||
// 'updatePeriod' is the time between pod updates.
|
// 'updatePeriod' is the time between pod updates.
|
||||||
|
@ -29,9 +29,8 @@ var storageToType = map[string]reflect.Type{
|
|||||||
"replicationControllers": reflect.TypeOf(api.ReplicationController{}),
|
"replicationControllers": reflect.TypeOf(api.ReplicationController{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Takes input 'data' as either json or yaml, checks that it parses as the
|
// ToWireFormat takes input 'data' as either json or yaml, checks that it parses as the
|
||||||
// appropriate object type, and returns json for sending to the API or an
|
// appropriate object type, and returns json for sending to the API or an error.
|
||||||
// error.
|
|
||||||
func ToWireFormat(data []byte, storage string) ([]byte, error) {
|
func ToWireFormat(data []byte, storage string) ([]byte, error) {
|
||||||
prototypeType, found := storageToType[storage]
|
prototypeType, found := storageToType[storage]
|
||||||
if !found {
|
if !found {
|
||||||
|
@ -31,26 +31,26 @@ func TestParseBadStorage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DoParseTest(t *testing.T, storage string, obj interface{}) {
|
func DoParseTest(t *testing.T, storage string, obj interface{}) {
|
||||||
json_data, _ := api.Encode(obj)
|
jsonData, _ := api.Encode(obj)
|
||||||
yaml_data, _ := yaml.Marshal(obj)
|
yamlData, _ := yaml.Marshal(obj)
|
||||||
t.Logf("Intermediate yaml:\n%v\n", string(yaml_data))
|
t.Logf("Intermediate yaml:\n%v\n", string(yamlData))
|
||||||
|
|
||||||
json_got, json_err := ToWireFormat(json_data, storage)
|
jsonGot, jsonErr := ToWireFormat(jsonData, storage)
|
||||||
yaml_got, yaml_err := ToWireFormat(yaml_data, storage)
|
yamlGot, yamlErr := ToWireFormat(yamlData, storage)
|
||||||
|
|
||||||
if json_err != nil {
|
if jsonErr != nil {
|
||||||
t.Errorf("json err: %#v", json_err)
|
t.Errorf("json err: %#v", jsonErr)
|
||||||
}
|
}
|
||||||
if yaml_err != nil {
|
if yamlErr != nil {
|
||||||
t.Errorf("yaml err: %#v", yaml_err)
|
t.Errorf("yaml err: %#v", yamlErr)
|
||||||
}
|
}
|
||||||
if string(json_got) != string(json_data) {
|
if string(jsonGot) != string(jsonData) {
|
||||||
t.Errorf("json output didn't match:\nGot:\n%v\n\nWanted:\n%v\n",
|
t.Errorf("json output didn't match:\nGot:\n%v\n\nWanted:\n%v\n",
|
||||||
string(json_got), string(json_data))
|
string(jsonGot), string(jsonData))
|
||||||
}
|
}
|
||||||
if string(yaml_got) != string(json_data) {
|
if string(yamlGot) != string(jsonData) {
|
||||||
t.Errorf("yaml parsed output didn't match:\nGot:\n%v\n\nWanted:\n%v\n",
|
t.Errorf("yaml parsed output didn't match:\nGot:\n%v\n\nWanted:\n%v\n",
|
||||||
string(yaml_got), string(json_data))
|
string(yamlGot), string(jsonData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProxyServer is a http.Handler which proxies Kubenetes APIs to remote API server.
|
||||||
type ProxyServer struct {
|
type ProxyServer struct {
|
||||||
Host string
|
Host string
|
||||||
Auth *client.AuthInfo
|
Auth *client.AuthInfo
|
||||||
@ -34,6 +35,8 @@ func makeFileHandler(prefix, base string) http.Handler {
|
|||||||
return http.StripPrefix(prefix, http.FileServer(http.Dir(base)))
|
return http.StripPrefix(prefix, http.FileServer(http.Dir(base)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewProxyServer creates and installs a new ProxyServer.
|
||||||
|
// It automatically registers the created ProxyServer to http.DefaultServeMux.
|
||||||
func NewProxyServer(filebase, host string, auth *client.AuthInfo) *ProxyServer {
|
func NewProxyServer(filebase, host string, auth *client.AuthInfo) *ProxyServer {
|
||||||
server := &ProxyServer{
|
server := &ProxyServer{
|
||||||
Host: host,
|
Host: host,
|
||||||
@ -45,7 +48,7 @@ func NewProxyServer(filebase, host string, auth *client.AuthInfo) *ProxyServer {
|
|||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts the server, loops forever.
|
// Serve starts the server (http.DefaultServeMux) on TCP port 8001, loops forever.
|
||||||
func (s *ProxyServer) Serve() error {
|
func (s *ProxyServer) Serve() error {
|
||||||
return http.ListenAndServe(":8001", nil)
|
return http.ListenAndServe(":8001", nil)
|
||||||
}
|
}
|
||||||
|
@ -35,14 +35,16 @@ type ResourcePrinter interface {
|
|||||||
PrintObj(interface{}, io.Writer) error
|
PrintObj(interface{}, io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identity printer simply copies the body out to the output stream
|
// IdentityPrinter is an implementation of ResourcePrinter which simply copies the body out to the output stream
|
||||||
type IdentityPrinter struct{}
|
type IdentityPrinter struct{}
|
||||||
|
|
||||||
|
// Print is an implementation of ResourcePrinter.Print which simply writes the data to the Writer.
|
||||||
func (i *IdentityPrinter) Print(data []byte, w io.Writer) error {
|
func (i *IdentityPrinter) Print(data []byte, w io.Writer) error {
|
||||||
_, err := w.Write(data)
|
_, err := w.Write(data)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrintObj is an implementation of ResourcePrinter.PrintObj which simply writes the object to the Writer.
|
||||||
func (i *IdentityPrinter) PrintObj(obj interface{}, output io.Writer) error {
|
func (i *IdentityPrinter) PrintObj(obj interface{}, output io.Writer) error {
|
||||||
data, err := api.Encode(obj)
|
data, err := api.Encode(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -51,9 +53,10 @@ func (i *IdentityPrinter) PrintObj(obj interface{}, output io.Writer) error {
|
|||||||
return i.Print(data, output)
|
return i.Print(data, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// YAMLPrinter parses JSON, and re-formats as YAML
|
// YAMLPrinter is an implementation of ResourcePrinter which parsess JSON, and re-formats as YAML
|
||||||
type YAMLPrinter struct{}
|
type YAMLPrinter struct{}
|
||||||
|
|
||||||
|
// Print parses the data as JSON, re-formats as YAML and prints the YAML.
|
||||||
func (y *YAMLPrinter) Print(data []byte, w io.Writer) error {
|
func (y *YAMLPrinter) Print(data []byte, w io.Writer) error {
|
||||||
var obj interface{}
|
var obj interface{}
|
||||||
if err := json.Unmarshal(data, &obj); err != nil {
|
if err := json.Unmarshal(data, &obj); err != nil {
|
||||||
@ -67,6 +70,7 @@ func (y *YAMLPrinter) Print(data []byte, w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrintObj prints the data as YAML.
|
||||||
func (y *YAMLPrinter) PrintObj(obj interface{}, w io.Writer) error {
|
func (y *YAMLPrinter) PrintObj(obj interface{}, w io.Writer) error {
|
||||||
output, err := yaml.Marshal(obj)
|
output, err := yaml.Marshal(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -76,7 +80,7 @@ func (y *YAMLPrinter) PrintObj(obj interface{}, w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// HumanReadablePrinter attempts to provide more elegant output
|
// HumanReadablePrinter is an implementation of ResourcePrinter which attempts to provide more elegant output.
|
||||||
type HumanReadablePrinter struct{}
|
type HumanReadablePrinter struct{}
|
||||||
|
|
||||||
var podColumns = []string{"Name", "Image(s)", "Host", "Labels"}
|
var podColumns = []string{"Name", "Image(s)", "Host", "Labels"}
|
||||||
@ -177,6 +181,7 @@ func (h *HumanReadablePrinter) printStatus(status *api.Status, w io.Writer) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Print parses the data as JSON, then prints the parsed data in a human-friendly format according to the type of the data.
|
||||||
func (h *HumanReadablePrinter) Print(data []byte, output io.Writer) error {
|
func (h *HumanReadablePrinter) Print(data []byte, output io.Writer) error {
|
||||||
var mapObj map[string]interface{}
|
var mapObj map[string]interface{}
|
||||||
if err := json.Unmarshal([]byte(data), &mapObj); err != nil {
|
if err := json.Unmarshal([]byte(data), &mapObj); err != nil {
|
||||||
@ -194,6 +199,7 @@ func (h *HumanReadablePrinter) Print(data []byte, output io.Writer) error {
|
|||||||
return h.PrintObj(obj, output)
|
return h.PrintObj(obj, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrintObj prints the obj in a human-friendly format according to the type of the obj.
|
||||||
func (h *HumanReadablePrinter) PrintObj(obj interface{}, output io.Writer) error {
|
func (h *HumanReadablePrinter) PrintObj(obj interface{}, output io.Writer) error {
|
||||||
w := tabwriter.NewWriter(output, 20, 5, 3, ' ', 0)
|
w := tabwriter.NewWriter(output, 20, 5, 3, ' ', 0)
|
||||||
defer w.Flush()
|
defer w.Flush()
|
||||||
|
@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/fsouza/go-dockerclient"
|
"github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A simple fake docker client, so that kubelet can be run for testing without requiring a real docker setup.
|
// FakeDockerClient is a simple fake docker client, so that kubelet can be run for testing without requiring a real docker setup.
|
||||||
type FakeDockerClient struct {
|
type FakeDockerClient struct {
|
||||||
containerList []docker.APIContainers
|
containerList []docker.APIContainers
|
||||||
container *docker.Container
|
container *docker.Container
|
||||||
@ -41,16 +41,22 @@ func (f *FakeDockerClient) appendCall(call string) {
|
|||||||
f.called = append(f.called, call)
|
f.called = append(f.called, call)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListContainers is a test-spy implementation of DockerInterface.ListContainers.
|
||||||
|
// It adds an entry "list" to the internal method call record.
|
||||||
func (f *FakeDockerClient) ListContainers(options docker.ListContainersOptions) ([]docker.APIContainers, error) {
|
func (f *FakeDockerClient) ListContainers(options docker.ListContainersOptions) ([]docker.APIContainers, error) {
|
||||||
f.appendCall("list")
|
f.appendCall("list")
|
||||||
return f.containerList, f.err
|
return f.containerList, f.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InspectContainer is a test-spy implementation of DockerInterface.InspectContainer.
|
||||||
|
// It adds an entry "inspect" to the internal method call record.
|
||||||
func (f *FakeDockerClient) InspectContainer(id string) (*docker.Container, error) {
|
func (f *FakeDockerClient) InspectContainer(id string) (*docker.Container, error) {
|
||||||
f.appendCall("inspect")
|
f.appendCall("inspect")
|
||||||
return f.container, f.err
|
return f.container, f.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateContainer is a test-spy implementation of DockerInterface.CreateContainer.
|
||||||
|
// It adds an entry "create" to the internal method call record.
|
||||||
func (f *FakeDockerClient) CreateContainer(c docker.CreateContainerOptions) (*docker.Container, error) {
|
func (f *FakeDockerClient) CreateContainer(c docker.CreateContainerOptions) (*docker.Container, error) {
|
||||||
f.appendCall("create")
|
f.appendCall("create")
|
||||||
f.Created = append(f.Created, c.Name)
|
f.Created = append(f.Created, c.Name)
|
||||||
@ -61,11 +67,15 @@ func (f *FakeDockerClient) CreateContainer(c docker.CreateContainerOptions) (*do
|
|||||||
return &docker.Container{ID: name}, nil
|
return &docker.Container{ID: name}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartContainer is a test-spy implementation of DockerInterface.StartContainer.
|
||||||
|
// It adds an entry "start" to the internal method call record.
|
||||||
func (f *FakeDockerClient) StartContainer(id string, hostConfig *docker.HostConfig) error {
|
func (f *FakeDockerClient) StartContainer(id string, hostConfig *docker.HostConfig) error {
|
||||||
f.appendCall("start")
|
f.appendCall("start")
|
||||||
return f.err
|
return f.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopContainer is a test-spy implementation of DockerInterface.StopContainer.
|
||||||
|
// It adds an entry "stop" to the internal method call record.
|
||||||
func (f *FakeDockerClient) StopContainer(id string, timeout uint) error {
|
func (f *FakeDockerClient) StopContainer(id string, timeout uint) error {
|
||||||
f.appendCall("stop")
|
f.appendCall("stop")
|
||||||
f.stopped = append(f.stopped, id)
|
f.stopped = append(f.stopped, id)
|
||||||
@ -79,12 +89,15 @@ func (f *FakeDockerClient) StopContainer(id string, timeout uint) error {
|
|||||||
return f.err
|
return f.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PullImage is a test-spy implementation of DockerInterface.StopContainer.
|
||||||
|
// It adds an entry "pull" to the internal method call record.
|
||||||
func (f *FakeDockerClient) PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error {
|
func (f *FakeDockerClient) PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error {
|
||||||
f.appendCall("pull")
|
f.appendCall("pull")
|
||||||
f.pulled = append(f.pulled, fmt.Sprintf("%s/%s:%s", opts.Repository, opts.Registry, opts.Tag))
|
f.pulled = append(f.pulled, fmt.Sprintf("%s/%s:%s", opts.Repository, opts.Registry, opts.Tag))
|
||||||
return f.err
|
return f.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FakeDockerPuller is a stub implementation of DockerPuller.
|
||||||
type FakeDockerPuller struct {
|
type FakeDockerPuller struct {
|
||||||
ImagesPulled []string
|
ImagesPulled []string
|
||||||
|
|
||||||
@ -93,7 +106,7 @@ type FakeDockerPuller struct {
|
|||||||
ErrorsToInject []error
|
ErrorsToInject []error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records the image pull attempt, and optionally injects an error.
|
// Pull records the image pull attempt, and optionally injects an error.
|
||||||
func (f *FakeDockerPuller) Pull(image string) error {
|
func (f *FakeDockerPuller) Pull(image string) error {
|
||||||
f.ImagesPulled = append(f.ImagesPulled, image)
|
f.ImagesPulled = append(f.ImagesPulled, image)
|
||||||
|
|
||||||
|
@ -25,7 +25,9 @@ import (
|
|||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HealthChecker is an abstract interface for container health checker.
|
||||||
type HealthChecker interface {
|
type HealthChecker interface {
|
||||||
|
// IsHealthy checks if the container is healthy.
|
||||||
IsHealthy(container api.Container) (bool, error)
|
IsHealthy(container api.Container) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +35,7 @@ type httpDoInterface interface {
|
|||||||
Get(string) (*http.Response, error)
|
Get(string) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeHealthChecker creates a new HealthChecker.
|
||||||
func MakeHealthChecker() HealthChecker {
|
func MakeHealthChecker() HealthChecker {
|
||||||
return &MuxHealthChecker{
|
return &MuxHealthChecker{
|
||||||
checkers: map[string]HealthChecker{
|
checkers: map[string]HealthChecker{
|
||||||
@ -43,10 +46,12 @@ func MakeHealthChecker() HealthChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MuxHealthChecker bundles multiple implementations of HealthChecker of different types.
|
||||||
type MuxHealthChecker struct {
|
type MuxHealthChecker struct {
|
||||||
checkers map[string]HealthChecker
|
checkers map[string]HealthChecker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHealthy checks the health of the container by delegating to an appropriate HealthChecker according to container.LivenessProbe.Type.
|
||||||
func (m *MuxHealthChecker) IsHealthy(container api.Container) (bool, error) {
|
func (m *MuxHealthChecker) IsHealthy(container api.Container) (bool, error) {
|
||||||
checker, ok := m.checkers[container.LivenessProbe.Type]
|
checker, ok := m.checkers[container.LivenessProbe.Type]
|
||||||
if !ok || checker == nil {
|
if !ok || checker == nil {
|
||||||
@ -56,6 +61,7 @@ func (m *MuxHealthChecker) IsHealthy(container api.Container) (bool, error) {
|
|||||||
return checker.IsHealthy(container)
|
return checker.IsHealthy(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPHealthChecker is an implementation of HealthChecker which checks container health by sending HTTP Get requests.
|
||||||
type HTTPHealthChecker struct {
|
type HTTPHealthChecker struct {
|
||||||
client httpDoInterface
|
client httpDoInterface
|
||||||
}
|
}
|
||||||
@ -70,6 +76,7 @@ func (h *HTTPHealthChecker) findPort(container api.Container, portName string) i
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHealthy checks if the container is healthy by trying sending HTTP Get requests to the container.
|
||||||
func (h *HTTPHealthChecker) IsHealthy(container api.Container) (bool, error) {
|
func (h *HTTPHealthChecker) IsHealthy(container api.Container) (bool, error) {
|
||||||
params := container.LivenessProbe.HTTPGet
|
params := container.LivenessProbe.HTTPGet
|
||||||
port := h.findPort(container, params.Port)
|
port := h.findPort(container, params.Port)
|
||||||
|
@ -23,19 +23,19 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeHttpClient struct {
|
type fakeHTTPClient struct {
|
||||||
req string
|
req string
|
||||||
res http.Response
|
res http.Response
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeHttpClient) Get(url string) (*http.Response, error) {
|
func (f *fakeHTTPClient) Get(url string) (*http.Response, error) {
|
||||||
f.req = url
|
f.req = url
|
||||||
return &f.res, f.err
|
return &f.res, f.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHttpHealth(t *testing.T) {
|
func TestHttpHealth(t *testing.T) {
|
||||||
fakeClient := fakeHttpClient{
|
fakeClient := fakeHTTPClient{
|
||||||
res: http.Response{
|
res: http.Response{
|
||||||
StatusCode: http.StatusOK,
|
StatusCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
|
@ -44,17 +44,14 @@ import (
|
|||||||
"gopkg.in/v1/yaml"
|
"gopkg.in/v1/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// State, sub object of the Docker JSON data
|
// DockerContainerData is the structured representation of the JSON object returned by Docker inspect
|
||||||
type State struct {
|
|
||||||
Running bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// The structured representation of the JSON object returned by Docker inspect
|
|
||||||
type DockerContainerData struct {
|
type DockerContainerData struct {
|
||||||
state State
|
state struct {
|
||||||
|
Running bool
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for testability
|
// DockerInterface is an abstract interface for testability. It abstracts the interface of docker.Client.
|
||||||
type DockerInterface interface {
|
type DockerInterface interface {
|
||||||
ListContainers(options docker.ListContainersOptions) ([]docker.APIContainers, error)
|
ListContainers(options docker.ListContainersOptions) ([]docker.APIContainers, error)
|
||||||
InspectContainer(id string) (*docker.Container, error)
|
InspectContainer(id string) (*docker.Container, error)
|
||||||
@ -64,24 +61,26 @@ type DockerInterface interface {
|
|||||||
PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error
|
PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type to make it clear when we're working with docker container Ids
|
// DockerID is an ID of docker container. It is a type to make it clear when we're working with docker container Ids
|
||||||
type DockerID string
|
type DockerID string
|
||||||
|
|
||||||
//Interface for testability
|
// DockerPuller is an abstract interface for testability. It abstracts image pull operations.
|
||||||
type DockerPuller interface {
|
type DockerPuller interface {
|
||||||
Pull(image string) error
|
Pull(image string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CadvisorInterface is an abstract interface for testability. It abstracts the interface of "github.com/google/cadvisor/client".Client.
|
||||||
type CadvisorInterface interface {
|
type CadvisorInterface interface {
|
||||||
ContainerInfo(name string) (*info.ContainerInfo, error)
|
ContainerInfo(name string) (*info.ContainerInfo, error)
|
||||||
MachineInfo() (*info.MachineInfo, error)
|
MachineInfo() (*info.MachineInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Kubelet.
|
||||||
func New() *Kubelet {
|
func New() *Kubelet {
|
||||||
return &Kubelet{}
|
return &Kubelet{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The main kubelet implementation
|
// Kubelet is the main kubelet implementation.
|
||||||
type Kubelet struct {
|
type Kubelet struct {
|
||||||
Hostname string
|
Hostname string
|
||||||
EtcdClient tools.EtcdClient
|
EtcdClient tools.EtcdClient
|
||||||
@ -107,9 +106,9 @@ const (
|
|||||||
httpServerSource = "http_server"
|
httpServerSource = "http_server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Starts background goroutines. If config_path, manifest_url, or address are empty,
|
// RunKubelet starts background goroutines. If config_path, manifest_url, or address are empty,
|
||||||
// they are not watched. Never returns.
|
// they are not watched. Never returns.
|
||||||
func (kl *Kubelet) RunKubelet(dockerEndpoint, config_path, manifest_url, etcd_servers, address string, port uint) {
|
func (kl *Kubelet) RunKubelet(dockerEndpoint, configPath, manifestURL, etcdServers, address string, port uint) {
|
||||||
if kl.CadvisorClient == nil {
|
if kl.CadvisorClient == nil {
|
||||||
var err error
|
var err error
|
||||||
kl.CadvisorClient, err = cadvisor.NewClient("http://127.0.0.1:5000")
|
kl.CadvisorClient, err = cadvisor.NewClient("http://127.0.0.1:5000")
|
||||||
@ -121,22 +120,22 @@ func (kl *Kubelet) RunKubelet(dockerEndpoint, config_path, manifest_url, etcd_se
|
|||||||
kl.DockerPuller = kl.MakeDockerPuller()
|
kl.DockerPuller = kl.MakeDockerPuller()
|
||||||
}
|
}
|
||||||
updateChannel := make(chan manifestUpdate)
|
updateChannel := make(chan manifestUpdate)
|
||||||
if config_path != "" {
|
if configPath != "" {
|
||||||
glog.Infof("Watching for file configs at %s", config_path)
|
glog.Infof("Watching for file configs at %s", configPath)
|
||||||
go util.Forever(func() {
|
go util.Forever(func() {
|
||||||
kl.WatchFiles(config_path, updateChannel)
|
kl.WatchFiles(configPath, updateChannel)
|
||||||
}, kl.FileCheckFrequency)
|
}, kl.FileCheckFrequency)
|
||||||
}
|
}
|
||||||
if manifest_url != "" {
|
if manifestURL != "" {
|
||||||
glog.Infof("Watching for HTTP configs at %s", manifest_url)
|
glog.Infof("Watching for HTTP configs at %s", manifestURL)
|
||||||
go util.Forever(func() {
|
go util.Forever(func() {
|
||||||
if err := kl.extractFromHTTP(manifest_url, updateChannel); err != nil {
|
if err := kl.extractFromHTTP(manifestURL, updateChannel); err != nil {
|
||||||
glog.Errorf("Error syncing http: %v", err)
|
glog.Errorf("Error syncing http: %v", err)
|
||||||
}
|
}
|
||||||
}, kl.HTTPCheckFrequency)
|
}, kl.HTTPCheckFrequency)
|
||||||
}
|
}
|
||||||
if etcd_servers != "" {
|
if etcdServers != "" {
|
||||||
servers := []string{etcd_servers}
|
servers := []string{etcdServers}
|
||||||
glog.Infof("Watching for etcd configs at %v", servers)
|
glog.Infof("Watching for etcd configs at %v", servers)
|
||||||
kl.EtcdClient = etcd.NewClient(servers)
|
kl.EtcdClient = etcd.NewClient(servers)
|
||||||
go util.Forever(func() { kl.SyncAndSetupEtcdWatch(updateChannel) }, 20*time.Second)
|
go util.Forever(func() { kl.SyncAndSetupEtcdWatch(updateChannel) }, 20*time.Second)
|
||||||
@ -160,15 +159,15 @@ func (kl *Kubelet) RunKubelet(dockerEndpoint, config_path, manifest_url, etcd_se
|
|||||||
kl.syncLoop(updateChannel, kl)
|
kl.syncLoop(updateChannel, kl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface implemented by Kubelet, for testability
|
// SyncHandler is an interface implemented by Kubelet, for testability
|
||||||
type SyncHandler interface {
|
type SyncHandler interface {
|
||||||
SyncManifests([]api.ContainerManifest) error
|
SyncManifests([]api.ContainerManifest) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log an event to the etcd backend.
|
// LogEvent logs an event to the etcd backend.
|
||||||
func (kl *Kubelet) LogEvent(event *api.Event) error {
|
func (kl *Kubelet) LogEvent(event *api.Event) error {
|
||||||
if kl.EtcdClient == nil {
|
if kl.EtcdClient == nil {
|
||||||
return fmt.Errorf("no etcd client connection.")
|
return fmt.Errorf("no etcd client connection")
|
||||||
}
|
}
|
||||||
event.Timestamp = time.Now().Unix()
|
event.Timestamp = time.Now().Unix()
|
||||||
data, err := json.Marshal(event)
|
data, err := json.Marshal(event)
|
||||||
@ -234,12 +233,14 @@ func (kl *Kubelet) getContainer(ID DockerID) (*docker.APIContainers, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeDockerPuller creates a new instance of the default implementation of DockerPuller.
|
||||||
func (kl *Kubelet) MakeDockerPuller() DockerPuller {
|
func (kl *Kubelet) MakeDockerPuller() DockerPuller {
|
||||||
return dockerPuller{
|
return dockerPuller{
|
||||||
client: kl.DockerClient,
|
client: kl.DockerClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dockerPuller is the default implementation of DockerPuller.
|
||||||
type dockerPuller struct {
|
type dockerPuller struct {
|
||||||
client DockerInterface
|
client DockerInterface
|
||||||
}
|
}
|
||||||
@ -304,7 +305,7 @@ func makeEnvironmentVariables(container *api.Container) []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeVolumesAndBinds(manifestId string, container *api.Container) (map[string]struct{}, []string) {
|
func makeVolumesAndBinds(manifestID string, container *api.Container) (map[string]struct{}, []string) {
|
||||||
volumes := map[string]struct{}{}
|
volumes := map[string]struct{}{}
|
||||||
binds := []string{}
|
binds := []string{}
|
||||||
for _, volume := range container.VolumeMounts {
|
for _, volume := range container.VolumeMounts {
|
||||||
@ -314,7 +315,7 @@ func makeVolumesAndBinds(manifestId string, container *api.Container) (map[strin
|
|||||||
basePath = fmt.Sprintf("%s:%s", volume.MountPath, volume.MountPath)
|
basePath = fmt.Sprintf("%s:%s", volume.MountPath, volume.MountPath)
|
||||||
} else {
|
} else {
|
||||||
volumes[volume.MountPath] = struct{}{}
|
volumes[volume.MountPath] = struct{}{}
|
||||||
basePath = fmt.Sprintf("/exports/%s/%s:%s", manifestId, volume.Name, volume.MountPath)
|
basePath = fmt.Sprintf("/exports/%s/%s:%s", manifestID, volume.Name, volume.MountPath)
|
||||||
}
|
}
|
||||||
if volume.ReadOnly {
|
if volume.ReadOnly {
|
||||||
basePath += ":ro"
|
basePath += ":ro"
|
||||||
@ -465,12 +466,12 @@ func (kl *Kubelet) extractFromDir(name string) ([]api.ContainerManifest, error)
|
|||||||
return manifests, nil
|
return manifests, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch a file or direcory of files for changes to the set of pods that
|
// WatchFiles watches a file or direcory of files for changes to the set of pods that
|
||||||
// should run on this Kubelet.
|
// should run on this Kubelet.
|
||||||
func (kl *Kubelet) WatchFiles(config_path string, updateChannel chan<- manifestUpdate) {
|
func (kl *Kubelet) WatchFiles(configPath string, updateChannel chan<- manifestUpdate) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
statInfo, err := os.Stat(config_path)
|
statInfo, err := os.Stat(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
glog.Errorf("Error accessing path: %v", err)
|
glog.Errorf("Error accessing path: %v", err)
|
||||||
@ -478,14 +479,14 @@ func (kl *Kubelet) WatchFiles(config_path string, updateChannel chan<- manifestU
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if statInfo.Mode().IsDir() {
|
if statInfo.Mode().IsDir() {
|
||||||
manifests, err := kl.extractFromDir(config_path)
|
manifests, err := kl.extractFromDir(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Error polling dir: %v", err)
|
glog.Errorf("Error polling dir: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateChannel <- manifestUpdate{fileSource, manifests}
|
updateChannel <- manifestUpdate{fileSource, manifests}
|
||||||
} else if statInfo.Mode().IsRegular() {
|
} else if statInfo.Mode().IsRegular() {
|
||||||
manifest, err := kl.extractFromFile(config_path)
|
manifest, err := kl.extractFromFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Error polling file: %v", err)
|
glog.Errorf("Error polling file: %v", err)
|
||||||
return
|
return
|
||||||
@ -548,8 +549,8 @@ func (kl *Kubelet) extractFromHTTP(url string, updateChannel chan<- manifestUpda
|
|||||||
url, string(data), singleErr, manifest, multiErr, manifests)
|
url, string(data), singleErr, manifest, multiErr, manifests)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take an etcd Response object, and turn it into a structured list of containers
|
// ResponseToManifests takes an etcd Response object, and turns it into a structured list of containers.
|
||||||
// Return a list of containers, or an error if one occurs.
|
// It returns a list of containers, or an error if one occurs.
|
||||||
func (kl *Kubelet) ResponseToManifests(response *etcd.Response) ([]api.ContainerManifest, error) {
|
func (kl *Kubelet) ResponseToManifests(response *etcd.Response) ([]api.ContainerManifest, error) {
|
||||||
if response.Node == nil || len(response.Node.Value) == 0 {
|
if response.Node == nil || len(response.Node.Value) == 0 {
|
||||||
return nil, fmt.Errorf("no nodes field: %v", response)
|
return nil, fmt.Errorf("no nodes field: %v", response)
|
||||||
@ -578,7 +579,7 @@ func (kl *Kubelet) getKubeletStateFromEtcd(key string, updateChannel chan<- mani
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync with etcd, and set up an etcd watch for new configurations
|
// SyncAndSetupEtcdWatch synchronizes with etcd, and sets up an etcd watch for new configurations.
|
||||||
// The channel to send new configurations across
|
// The channel to send new configurations across
|
||||||
// This function loops forever and is intended to be run in a go routine.
|
// This function loops forever and is intended to be run in a go routine.
|
||||||
func (kl *Kubelet) SyncAndSetupEtcdWatch(updateChannel chan<- manifestUpdate) {
|
func (kl *Kubelet) SyncAndSetupEtcdWatch(updateChannel chan<- manifestUpdate) {
|
||||||
@ -610,7 +611,7 @@ func (kl *Kubelet) SyncAndSetupEtcdWatch(updateChannel chan<- manifestUpdate) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout the watch after 30 seconds
|
// TimeoutWatch timeout the watch after 30 seconds.
|
||||||
func (kl *Kubelet) TimeoutWatch(done chan bool) {
|
func (kl *Kubelet) TimeoutWatch(done chan bool) {
|
||||||
t := time.Tick(30 * time.Second)
|
t := time.Tick(30 * time.Second)
|
||||||
for _ = range t {
|
for _ = range t {
|
||||||
@ -618,7 +619,7 @@ func (kl *Kubelet) TimeoutWatch(done chan bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract data from YAML file into a list of containers.
|
// ExtractYAMLData extracts data from YAML file into a list of containers.
|
||||||
func (kl *Kubelet) ExtractYAMLData(buf []byte, output interface{}) error {
|
func (kl *Kubelet) ExtractYAMLData(buf []byte, output interface{}) error {
|
||||||
err := yaml.Unmarshal(buf, output)
|
err := yaml.Unmarshal(buf, output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -637,7 +638,7 @@ func (kl *Kubelet) extractFromEtcd(response *etcd.Response) ([]api.ContainerMani
|
|||||||
return manifests, err
|
return manifests, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch etcd for changes, receives config objects from the etcd client watch.
|
// WatchEtcd watches etcd for changes, receives config objects from the etcd client watch.
|
||||||
// This function loops until the watchChannel is closed, and is intended to be run as a goroutine.
|
// This function loops until the watchChannel is closed, and is intended to be run as a goroutine.
|
||||||
func (kl *Kubelet) WatchEtcd(watchChannel <-chan *etcd.Response, updateChannel chan<- manifestUpdate) {
|
func (kl *Kubelet) WatchEtcd(watchChannel <-chan *etcd.Response, updateChannel chan<- manifestUpdate) {
|
||||||
defer util.HandleCrash()
|
defer util.HandleCrash()
|
||||||
@ -751,7 +752,7 @@ func (kl *Kubelet) syncManifest(manifest *api.ContainerManifest, keepChannel cha
|
|||||||
|
|
||||||
type empty struct{}
|
type empty struct{}
|
||||||
|
|
||||||
// Sync the configured list of containers (desired state) with the host current state
|
// SyncManifests synchronizes the configured list of containers (desired state) with the host current state.
|
||||||
func (kl *Kubelet) SyncManifests(config []api.ContainerManifest) error {
|
func (kl *Kubelet) SyncManifests(config []api.ContainerManifest) error {
|
||||||
glog.Infof("Desired: %+v", config)
|
glog.Infof("Desired: %+v", config)
|
||||||
var err error
|
var err error
|
||||||
@ -873,12 +874,12 @@ func (kl *Kubelet) syncLoop(updateChannel <-chan manifestUpdate, handler SyncHan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getContainerIdFromName looks at the list of containers on the machine and returns the ID of the container whose name
|
// getContainerIDFromName looks at the list of containers on the machine and returns the ID of the container whose name
|
||||||
// matches 'name'. It returns the name of the container, or empty string, if the container isn't found.
|
// matches 'name'. It returns the name of the container, or empty string, if the container isn't found.
|
||||||
// it returns true if the container is found, false otherwise, and any error that occurs.
|
// it returns true if the container is found, false otherwise, and any error that occurs.
|
||||||
// TODO: This functions exists to support GetContainerInfo and GetContainerStats
|
// TODO: This functions exists to support GetContainerInfo and GetContainerStats
|
||||||
// It should be removed once those two functions start taking proper pod.IDs
|
// It should be removed once those two functions start taking proper pod.IDs
|
||||||
func (kl *Kubelet) getContainerIdFromName(name string) (DockerID, bool, error) {
|
func (kl *Kubelet) getContainerIDFromName(name string) (DockerID, bool, error) {
|
||||||
containerList, err := kl.DockerClient.ListContainers(docker.ListContainersOptions{})
|
containerList, err := kl.DockerClient.ListContainers(docker.ListContainersOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
@ -891,7 +892,7 @@ func (kl *Kubelet) getContainerIdFromName(name string) (DockerID, bool, error) {
|
|||||||
return "", false, nil
|
return "", false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns docker info for all containers in the pod/manifest
|
// GetPodInfo returns docker info for all containers in the pod/manifest.
|
||||||
func (kl *Kubelet) GetPodInfo(podID string) (api.PodInfo, error) {
|
func (kl *Kubelet) GetPodInfo(podID string) (api.PodInfo, error) {
|
||||||
info := api.PodInfo{}
|
info := api.PodInfo{}
|
||||||
|
|
||||||
@ -970,7 +971,7 @@ func (kl *Kubelet) statsFromContainerPath(containerPath string) (*api.ContainerS
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns stats (from Cadvisor) for a container.
|
// GetContainerStats returns stats (from Cadvisor) for a container.
|
||||||
func (kl *Kubelet) GetContainerStats(podID, containerName string) (*api.ContainerStats, error) {
|
func (kl *Kubelet) GetContainerStats(podID, containerName string) (*api.ContainerStats, error) {
|
||||||
if kl.CadvisorClient == nil {
|
if kl.CadvisorClient == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -982,7 +983,7 @@ func (kl *Kubelet) GetContainerStats(podID, containerName string) (*api.Containe
|
|||||||
return kl.statsFromContainerPath(fmt.Sprintf("/docker/%s", string(dockerID)))
|
return kl.statsFromContainerPath(fmt.Sprintf("/docker/%s", string(dockerID)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns stats (from Cadvisor) of current machine.
|
// GetMachineStats returns stats (from Cadvisor) of current machine.
|
||||||
func (kl *Kubelet) GetMachineStats() (*api.ContainerStats, error) {
|
func (kl *Kubelet) GetMachineStats() (*api.ContainerStats, error) {
|
||||||
return kl.statsFromContainerPath("/")
|
return kl.statsFromContainerPath("/")
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"gopkg.in/v1/yaml"
|
"gopkg.in/v1/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// KubeletServer is a http.Handler which exposes kubelet functionality over HTTP.
|
||||||
type KubeletServer struct {
|
type KubeletServer struct {
|
||||||
Kubelet kubeletInterface
|
Kubelet kubeletInterface
|
||||||
UpdateChannel chan<- manifestUpdate
|
UpdateChannel chan<- manifestUpdate
|
||||||
|
@ -54,7 +54,7 @@ type serverTestFramework struct {
|
|||||||
updateReader *channelReader
|
updateReader *channelReader
|
||||||
serverUnderTest *KubeletServer
|
serverUnderTest *KubeletServer
|
||||||
fakeKubelet *fakeKubelet
|
fakeKubelet *fakeKubelet
|
||||||
testHttpServer *httptest.Server
|
testHTTPServer *httptest.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeServerTest() *serverTestFramework {
|
func makeServerTest() *serverTestFramework {
|
||||||
@ -67,7 +67,7 @@ func makeServerTest() *serverTestFramework {
|
|||||||
Kubelet: fw.fakeKubelet,
|
Kubelet: fw.fakeKubelet,
|
||||||
UpdateChannel: fw.updateChan,
|
UpdateChannel: fw.updateChan,
|
||||||
}
|
}
|
||||||
fw.testHttpServer = httptest.NewServer(fw.serverUnderTest)
|
fw.testHTTPServer = httptest.NewServer(fw.serverUnderTest)
|
||||||
return fw
|
return fw
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ func TestContainer(t *testing.T) {
|
|||||||
{ID: "test_manifest"},
|
{ID: "test_manifest"},
|
||||||
}
|
}
|
||||||
body := bytes.NewBuffer([]byte(util.MakeJSONString(expected[0]))) // Only send a single ContainerManifest
|
body := bytes.NewBuffer([]byte(util.MakeJSONString(expected[0]))) // Only send a single ContainerManifest
|
||||||
resp, err := http.Post(fw.testHttpServer.URL+"/container", "application/json", body)
|
resp, err := http.Post(fw.testHTTPServer.URL+"/container", "application/json", body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Post returned: %v", err)
|
t.Errorf("Post returned: %v", err)
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ func TestContainers(t *testing.T) {
|
|||||||
{ID: "test_manifest_2"},
|
{ID: "test_manifest_2"},
|
||||||
}
|
}
|
||||||
body := bytes.NewBuffer([]byte(util.MakeJSONString(expected)))
|
body := bytes.NewBuffer([]byte(util.MakeJSONString(expected)))
|
||||||
resp, err := http.Post(fw.testHttpServer.URL+"/containers", "application/json", body)
|
resp, err := http.Post(fw.testHTTPServer.URL+"/containers", "application/json", body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Post returned: %v", err)
|
t.Errorf("Post returned: %v", err)
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ func TestPodInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("bad pod")
|
return nil, fmt.Errorf("bad pod")
|
||||||
}
|
}
|
||||||
resp, err := http.Get(fw.testHttpServer.URL + "/podInfo?podID=goodpod")
|
resp, err := http.Get(fw.testHTTPServer.URL + "/podInfo?podID=goodpod")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Got error GETing: %v", err)
|
t.Errorf("Got error GETing: %v", err)
|
||||||
}
|
}
|
||||||
@ -170,7 +170,7 @@ func TestContainerStats(t *testing.T) {
|
|||||||
return expectedStats, nil
|
return expectedStats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(fw.testHttpServer.URL + fmt.Sprintf("/stats/%v/%v", expectedPodID, expectedContainerName))
|
resp, err := http.Get(fw.testHTTPServer.URL + fmt.Sprintf("/stats/%v/%v", expectedPodID, expectedContainerName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Got error GETing: %v", err)
|
t.Fatalf("Got error GETing: %v", err)
|
||||||
}
|
}
|
||||||
@ -205,7 +205,7 @@ func TestMachineStats(t *testing.T) {
|
|||||||
return expectedStats, nil
|
return expectedStats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(fw.testHttpServer.URL + "/stats")
|
resp, err := http.Get(fw.testHTTPServer.URL + "/stats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Got error GETing: %v", err)
|
t.Fatalf("Got error GETing: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -913,13 +913,15 @@ type mockCadvisorClient struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *mockCadvisorClient) ContainerInfo(name string) (*info.ContainerInfo, error) {
|
// ContainerInfo is a mock implementation of CadvisorInterface.ContainerInfo.
|
||||||
args := self.Called(name)
|
func (c *mockCadvisorClient) ContainerInfo(name string) (*info.ContainerInfo, error) {
|
||||||
|
args := c.Called(name)
|
||||||
return args.Get(0).(*info.ContainerInfo), args.Error(1)
|
return args.Get(0).(*info.ContainerInfo), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *mockCadvisorClient) MachineInfo() (*info.MachineInfo, error) {
|
// MachineInfo is a mock implementation of CadvisorInterface.MachineInfo.
|
||||||
args := self.Called()
|
func (c *mockCadvisorClient) MachineInfo() (*info.MachineInfo, error) {
|
||||||
|
args := c.Called()
|
||||||
return args.Get(0).(*info.MachineInfo), args.Error(1)
|
return args.Get(0).(*info.MachineInfo), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user