2015-01-13 02:11:27 +00:00
/ *
2015-05-01 16:19:44 +00:00
Copyright 2014 The Kubernetes Authors All rights reserved .
2015-01-13 02:11:27 +00:00
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 (
2015-03-26 20:34:18 +00:00
"bytes"
2015-02-08 15:57:48 +00:00
"fmt"
2015-05-26 14:24:46 +00:00
"io/ioutil"
2015-05-06 15:03:22 +00:00
"math"
2015-01-24 01:06:13 +00:00
"math/rand"
2015-05-26 14:24:46 +00:00
"net/http"
2015-04-24 17:26:12 +00:00
"os"
2015-03-26 20:34:18 +00:00
"os/exec"
2015-01-13 02:11:27 +00:00
"path/filepath"
2015-05-25 09:43:26 +00:00
"sort"
2015-01-24 01:06:13 +00:00
"strconv"
2015-03-26 20:34:18 +00:00
"strings"
2015-01-13 02:11:27 +00:00
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
2015-06-01 12:24:44 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
2015-03-06 22:49:25 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
2015-05-19 13:59:46 +00:00
clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api"
2015-05-19 16:13:08 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
2015-04-22 19:08:08 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
2015-05-05 14:48:50 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
2015-04-22 19:08:08 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
2015-06-01 12:24:44 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
2015-05-06 20:50:36 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
2015-06-08 08:52:37 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
2015-06-01 12:24:44 +00:00
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
2015-03-26 20:34:18 +00:00
2015-05-29 00:03:55 +00:00
"github.com/davecgh/go-spew/spew"
2015-04-24 17:26:12 +00:00
"golang.org/x/crypto/ssh"
2015-02-08 15:57:48 +00:00
. "github.com/onsi/ginkgo"
2015-02-12 18:37:31 +00:00
. "github.com/onsi/gomega"
2015-01-13 02:11:27 +00:00
)
2015-03-05 20:04:00 +00:00
const (
// Initial pod start can be delayed O(minutes) by slow docker pulls
// TODO: Make this 30 seconds once #4566 is resolved.
podStartTimeout = 5 * time . Minute
2015-05-08 00:11:48 +00:00
// String used to mark pod deletion
nonExist = "NonExist"
2015-04-29 22:28:48 +00:00
2015-05-21 21:43:42 +00:00
// How often to poll pods and nodes.
poll = 5 * time . Second
2015-05-22 20:46:52 +00:00
// service accounts are provisioned after namespace creation
// a service account is required to support pod creation in a namespace as part of admission control
serviceAccountProvisionTimeout = 2 * time . Minute
2015-05-21 21:43:42 +00:00
// How long to try single API calls (like 'get' or 'list'). Used to prevent
// transient failures from failing tests.
singleCallTimeout = 30 * time . Second
// How long nodes have to be "ready" when a test begins. They should already
// be "ready" before the test starts, so this is small.
nodeReadyInitialTimeout = 20 * time . Second
// How long pods have to be "ready" when a test begins. They should already
// be "ready" before the test starts, so this is small.
podReadyBeforeTimeout = 20 * time . Second
// How wide to print pod names, by default. Useful for aligning printing to
// quickly scan through output.
podPrintWidth = 55
2015-03-05 20:04:00 +00:00
)
2015-05-19 16:13:08 +00:00
type CloudConfig struct {
2015-04-23 14:28:16 +00:00
ProjectID string
Zone string
2015-06-19 22:17:28 +00:00
Cluster string
2015-04-23 14:28:16 +00:00
MasterName string
NodeInstanceGroup string
NumNodes int
2015-05-23 00:12:53 +00:00
ClusterTag string
2015-05-19 16:13:08 +00:00
Provider cloudprovider . Interface
}
2015-03-31 23:36:31 +00:00
type TestContextType struct {
2015-06-10 23:35:39 +00:00
KubeConfig string
KubeContext string
CertDir string
Host string
RepoRoot string
Provider string
CloudConfig CloudConfig
KubectlPath string
OutputDir string
prefix string
MinStartupPods int
2015-01-13 02:11:27 +00:00
}
2015-03-31 23:36:31 +00:00
var testContext TestContextType
2015-01-13 02:11:27 +00:00
2015-05-14 13:18:24 +00:00
type ContainerFailures struct {
status * api . ContainerStateTerminated
restarts int
}
2015-06-01 12:24:44 +00:00
// Convenient wrapper around cache.Store that returns list of api.Pod instead of interface{}.
type podStore struct {
cache . Store
stopCh chan struct { }
}
func newPodStore ( c * client . Client , namespace string , label labels . Selector , field fields . Selector ) * podStore {
lw := & cache . ListWatch {
ListFunc : func ( ) ( runtime . Object , error ) {
return c . Pods ( namespace ) . List ( label , field )
} ,
WatchFunc : func ( rv string ) ( watch . Interface , error ) {
return c . Pods ( namespace ) . Watch ( label , field , rv )
} ,
}
store := cache . NewStore ( cache . MetaNamespaceKeyFunc )
stopCh := make ( chan struct { } )
cache . NewReflector ( lw , & api . Pod { } , store , 0 ) . RunUntil ( stopCh )
return & podStore { store , stopCh }
}
func ( s * podStore ) List ( ) [ ] * api . Pod {
objects := s . Store . List ( )
pods := make ( [ ] * api . Pod , 0 )
for _ , o := range objects {
pods = append ( pods , o . ( * api . Pod ) )
}
return pods
}
func ( s * podStore ) Stop ( ) {
close ( s . stopCh )
}
2015-05-26 14:24:46 +00:00
type RCConfig struct {
Client * client . Client
Image string
Name string
Namespace string
2015-06-10 11:59:30 +00:00
PollInterval time . Duration
2015-05-26 14:24:46 +00:00
PodStatusFile * os . File
Replicas int
2015-06-11 22:55:25 +00:00
// Env vars, set the same for every pod.
Env map [ string ] string
// Extra labels added to every pod.
Labels map [ string ] string
// Ports to declare in the container (map of name to containerPort).
Ports map [ string ] int
// Pointer to a list of pods; if non-nil, will be set to a list of pods
// created by this RC by RunRC.
CreatedPods * [ ] * api . Pod
2015-05-26 14:24:46 +00:00
}
2015-02-10 00:57:06 +00:00
func Logf ( format string , a ... interface { } ) {
fmt . Fprintf ( GinkgoWriter , "INFO: " + format + "\n" , a ... )
}
2015-02-10 01:02:11 +00:00
func Failf ( format string , a ... interface { } ) {
2015-02-12 18:35:12 +00:00
Fail ( fmt . Sprintf ( format , a ... ) , 1 )
2015-02-10 01:02:11 +00:00
}
2015-04-18 22:30:10 +00:00
func providerIs ( providers ... string ) bool {
if testContext . Provider == "" {
Fail ( "testContext.Provider is not defined" )
}
for _ , provider := range providers {
if strings . ToLower ( provider ) == strings . ToLower ( testContext . Provider ) {
return true
}
}
return false
}
2015-03-05 20:04:00 +00:00
type podCondition func ( pod * api . Pod ) ( bool , error )
2015-05-18 20:49:32 +00:00
// podReady returns whether pod has a condition of Ready with a status of true.
func podReady ( pod * api . Pod ) bool {
for _ , cond := range pod . Status . Conditions {
if cond . Type == api . PodReady && cond . Status == api . ConditionTrue {
return true
}
}
return false
}
2015-05-21 22:59:35 +00:00
// logPodStates logs basic info of provided pods for debugging.
func logPodStates ( pods [ ] api . Pod ) {
// Find maximum widths for pod, node, and phase strings for column printing.
maxPodW , maxNodeW , maxPhaseW := len ( "POD" ) , len ( "NODE" ) , len ( "PHASE" )
for _ , pod := range pods {
if len ( pod . ObjectMeta . Name ) > maxPodW {
maxPodW = len ( pod . ObjectMeta . Name )
}
2015-05-22 23:40:57 +00:00
if len ( pod . Spec . NodeName ) > maxNodeW {
maxNodeW = len ( pod . Spec . NodeName )
2015-05-21 22:59:35 +00:00
}
if len ( pod . Status . Phase ) > maxPhaseW {
maxPhaseW = len ( pod . Status . Phase )
}
2015-05-18 20:49:32 +00:00
}
2015-05-21 22:59:35 +00:00
// Increase widths by one to separate by a single space.
maxPodW ++
maxNodeW ++
maxPhaseW ++
// Log pod info. * does space padding, - makes them left-aligned.
Logf ( "%-[1]*[2]s %-[3]*[4]s %-[5]*[6]s %[7]s" ,
maxPodW , "POD" , maxNodeW , "NODE" , maxPhaseW , "PHASE" , "CONDITIONS" )
for _ , pod := range pods {
Logf ( "%-[1]*[2]s %-[3]*[4]s %-[5]*[6]s %[7]s" ,
2015-05-22 23:40:57 +00:00
maxPodW , pod . ObjectMeta . Name , maxNodeW , pod . Spec . NodeName , maxPhaseW , pod . Status . Phase , pod . Status . Conditions )
2015-05-18 20:49:32 +00:00
}
2015-05-21 22:59:35 +00:00
Logf ( "" ) // Final empty line helps for readability.
2015-05-18 20:49:32 +00:00
}
// podRunningReady checks whether pod p's phase is running and it has a ready
// condition of status true.
func podRunningReady ( p * api . Pod ) ( bool , error ) {
// Check the phase is running.
if p . Status . Phase != api . PodRunning {
return false , fmt . Errorf ( "want pod '%s' on '%s' to be '%v' but was '%v'" ,
2015-05-22 23:40:57 +00:00
p . ObjectMeta . Name , p . Spec . NodeName , api . PodRunning , p . Status . Phase )
2015-05-18 20:49:32 +00:00
}
// Check the ready condition is true.
if ! podReady ( p ) {
return false , fmt . Errorf ( "pod '%s' on '%s' didn't have condition {%v %v}; conditions: %v" ,
2015-05-22 23:40:57 +00:00
p . ObjectMeta . Name , p . Spec . NodeName , api . PodReady , api . ConditionTrue , p . Status . Conditions )
2015-05-18 20:49:32 +00:00
}
return true , nil
}
// waitForPodsRunningReady waits up to timeout to ensure that all pods in
// namespace ns are running and ready, requiring that it finds at least minPods.
// It has separate behavior from other 'wait for' pods functions in that it re-
// queries the list of pods on every iteration. This is useful, for example, in
// cluster startup, because the number of pods increases while waiting.
func waitForPodsRunningReady ( ns string , minPods int , timeout time . Duration ) error {
c , err := loadClient ( )
if err != nil {
return err
}
2015-05-21 21:43:42 +00:00
start := time . Now ( )
2015-05-18 20:49:32 +00:00
Logf ( "Waiting up to %v for all pods (need at least %d) in namespace '%s' to be running and ready" ,
timeout , minPods , ns )
2015-05-21 21:43:42 +00:00
if wait . Poll ( poll , timeout , func ( ) ( bool , error ) {
2015-05-18 20:49:32 +00:00
// We get the new list of pods in every iteration beause more pods come
// online during startup and we want to ensure they are also checked.
podList , err := c . Pods ( ns ) . List ( labels . Everything ( ) , fields . Everything ( ) )
if err != nil {
Logf ( "Error getting pods in namespace '%s': %v" , ns , err )
2015-05-21 21:43:42 +00:00
return false , nil
2015-05-18 20:49:32 +00:00
}
2015-05-21 22:59:35 +00:00
nOk , badPods := 0 , [ ] api . Pod { }
2015-05-18 20:49:32 +00:00
for _ , pod := range podList . Items {
if res , err := podRunningReady ( & pod ) ; res && err == nil {
nOk ++
2015-05-21 22:59:35 +00:00
} else {
badPods = append ( badPods , pod )
2015-05-18 20:49:32 +00:00
}
}
2015-05-21 22:59:35 +00:00
Logf ( "%d / %d pods in namespace '%s' are running and ready (%d seconds elapsed)" ,
nOk , len ( podList . Items ) , ns , int ( time . Since ( start ) . Seconds ( ) ) )
2015-05-18 20:49:32 +00:00
if nOk == len ( podList . Items ) && nOk >= minPods {
2015-05-21 21:43:42 +00:00
return true , nil
2015-05-18 20:49:32 +00:00
}
2015-05-21 22:59:35 +00:00
logPodStates ( badPods )
2015-05-21 21:43:42 +00:00
return false , nil
} ) != nil {
return fmt . Errorf ( "Not all pods in namespace '%s' running and ready within %v" , ns , timeout )
2015-05-18 20:49:32 +00:00
}
2015-05-21 21:43:42 +00:00
return nil
2015-05-18 20:49:32 +00:00
}
2015-05-21 21:43:42 +00:00
func waitForServiceAccountInNamespace ( c * client . Client , ns , serviceAccountName string , timeout time . Duration ) error {
2015-05-22 20:46:52 +00:00
Logf ( "Waiting up to %v for service account %s to be provisioned in ns %s" , timeout , serviceAccountName , ns )
for start := time . Now ( ) ; time . Since ( start ) < timeout ; time . Sleep ( poll ) {
_ , err := c . ServiceAccounts ( ns ) . Get ( serviceAccountName )
if err != nil {
Logf ( "Get service account %s in ns %s failed, ignoring for %v: %v" , serviceAccountName , ns , poll , err )
continue
}
Logf ( "Service account %s in ns %s found. (%v)" , serviceAccountName , ns , time . Since ( start ) )
return nil
}
return fmt . Errorf ( "Service account %s in namespace %s not ready within %v" , serviceAccountName , ns , timeout )
}
2015-05-21 21:43:42 +00:00
func waitForPodCondition ( c * client . Client , ns , podName , desc string , timeout time . Duration , condition podCondition ) error {
Logf ( "Waiting up to %[1]v for pod %-[2]*[3]s status to be %[4]s" , timeout , podPrintWidth , podName , desc )
2015-04-29 22:28:48 +00:00
for start := time . Now ( ) ; time . Since ( start ) < timeout ; time . Sleep ( poll ) {
2015-03-05 20:04:00 +00:00
pod , err := c . Pods ( ns ) . Get ( podName )
2015-01-13 02:11:27 +00:00
if err != nil {
2015-05-21 21:43:42 +00:00
// Aligning this text makes it much more readable
Logf ( "Get pod %-[1]*[2]s in namespace '%[3]s' failed, ignoring for %[4]v. Error: %[5]v" ,
podPrintWidth , podName , ns , poll , err )
2015-03-05 20:04:00 +00:00
continue
2015-01-13 02:11:27 +00:00
}
2015-03-05 20:04:00 +00:00
done , err := condition ( pod )
if done {
return err
2015-01-13 02:11:27 +00:00
}
2015-05-21 21:43:42 +00:00
Logf ( "Waiting for pod %-[1]*[2]s in namespace '%[3]s' status to be '%[4]s'" +
"(found phase: %[5]q, readiness: %[6]t) (%[7]v elapsed)" ,
podPrintWidth , podName , ns , desc , pod . Status . Phase , podReady ( pod ) , time . Since ( start ) )
2015-01-13 02:11:27 +00:00
}
2015-05-18 20:49:32 +00:00
return fmt . Errorf ( "gave up waiting for pod '%s' to be '%s' after %v" , podName , desc , timeout )
2015-01-13 02:11:27 +00:00
}
2015-05-22 20:46:52 +00:00
// waitForDefaultServiceAccountInNamespace waits for the default service account to be provisioned
// the default service account is what is associated with pods when they do not specify a service account
// as a result, pods are not able to be provisioned in a namespace until the service account is provisioned
func waitForDefaultServiceAccountInNamespace ( c * client . Client , namespace string ) error {
2015-05-21 21:43:42 +00:00
return waitForServiceAccountInNamespace ( c , namespace , "default" , serviceAccountProvisionTimeout )
2015-05-22 20:46:52 +00:00
}
2015-06-05 01:40:41 +00:00
// waitForPersistentVolumePhase waits for a PersistentVolume to be in a specific phase or until timeout occurs, whichever comes first.
func waitForPersistentVolumePhase ( phase api . PersistentVolumePhase , c * client . Client , pvName string , poll , timeout time . Duration ) error {
Logf ( "Waiting up to %v for PersistentVolume %s to have phase %s" , timeout , pvName , phase )
for start := time . Now ( ) ; time . Since ( start ) < timeout ; time . Sleep ( poll ) {
pv , err := c . PersistentVolumes ( ) . Get ( pvName )
if err != nil {
Logf ( "Get persistent volume %s in failed, ignoring for %v: %v" , pvName , poll , err )
continue
} else {
if pv . Status . Phase == phase {
Logf ( "PersistentVolume %s found and phase=%s (%v)" , pvName , phase , time . Since ( start ) )
return nil
} else {
Logf ( "PersistentVolume %s found but phase is %s instead of %s." , pvName , pv . Status . Phase , phase )
}
}
}
return fmt . Errorf ( "PersistentVolume %s not in phase %s within %v" , pvName , phase , timeout )
}
2015-06-08 15:13:57 +00:00
// createTestingNS should be used by every test, note that we append a common prefix to the provided test name.
2015-05-29 00:03:55 +00:00
// Please see NewFramework instead of using this directly.
2015-04-17 15:22:29 +00:00
func createTestingNS ( baseName string , c * client . Client ) ( * api . Namespace , error ) {
namespaceObj := & api . Namespace {
ObjectMeta : api . ObjectMeta {
2015-06-08 17:51:32 +00:00
GenerateName : fmt . Sprintf ( "e2e-tests-%v-" , baseName ) ,
Namespace : "" ,
2015-04-17 15:22:29 +00:00
} ,
Status : api . NamespaceStatus { } ,
}
2015-06-08 17:51:32 +00:00
got , err := c . Namespaces ( ) . Create ( namespaceObj )
2015-06-05 17:27:26 +00:00
if err != nil {
2015-06-08 17:51:32 +00:00
return got , err
2015-06-05 17:27:26 +00:00
}
2015-06-08 17:51:32 +00:00
if err := waitForDefaultServiceAccountInNamespace ( c , got . Name ) ; err != nil {
return got , err
2015-06-05 17:27:26 +00:00
}
2015-06-08 17:51:32 +00:00
return got , nil
2015-04-17 15:22:29 +00:00
}
2015-06-22 11:47:05 +00:00
// deleteTestingNS checks whether all e2e based existing namespaces are in the Terminating state
// and waits until they are finally deleted.
func deleteTestingNS ( c * client . Client ) error {
Logf ( "Waiting for terminating namespaces to be deleted..." )
for start := time . Now ( ) ; time . Since ( start ) < 30 * time . Minute ; time . Sleep ( 15 * time . Second ) {
namespaces , err := c . Namespaces ( ) . List ( labels . Everything ( ) , fields . Everything ( ) )
if err != nil {
Logf ( "Listing namespaces failed: %v" , err )
continue
}
terminating := 0
for _ , ns := range namespaces . Items {
if strings . HasPrefix ( ns . ObjectMeta . Name , "e2e-tests-" ) {
if ns . Status . Phase == api . NamespaceActive {
return fmt . Errorf ( "Namespace %s is active" , ns )
}
terminating ++
}
}
if terminating == 0 {
return nil
}
}
return fmt . Errorf ( "Waiting for terminating namespaces to be deleted timed out" )
}
2015-03-19 00:46:31 +00:00
func waitForPodRunningInNamespace ( c * client . Client , podName string , namespace string ) error {
2015-05-21 21:43:42 +00:00
return waitForPodCondition ( c , namespace , podName , "running" , podStartTimeout , func ( pod * api . Pod ) ( bool , error ) {
2015-05-27 23:07:26 +00:00
if pod . Status . Phase == api . PodRunning {
2015-06-18 11:18:21 +00:00
Logf ( "Found pod '%s' on node '%s'" , podName , pod . Spec . NodeName )
2015-05-27 23:07:26 +00:00
return true , nil
}
if pod . Status . Phase == api . PodFailed {
2015-05-29 00:03:55 +00:00
return true , fmt . Errorf ( "Giving up; pod went into failed status: \n%s" , spew . Sprintf ( "%#v" , pod ) )
2015-05-27 23:07:26 +00:00
}
return false , nil
2015-03-05 20:04:00 +00:00
} )
}
2015-03-19 00:46:31 +00:00
func waitForPodRunning ( c * client . Client , podName string ) error {
return waitForPodRunningInNamespace ( c , podName , api . NamespaceDefault )
}
2015-03-05 20:04:00 +00:00
// waitForPodNotPending returns an error if it took too long for the pod to go out of pending state.
func waitForPodNotPending ( c * client . Client , ns , podName string ) error {
2015-05-21 21:43:42 +00:00
return waitForPodCondition ( c , ns , podName , "!pending" , podStartTimeout , func ( pod * api . Pod ) ( bool , error ) {
2015-01-22 16:15:36 +00:00
if pod . Status . Phase != api . PodPending {
2015-05-18 20:49:32 +00:00
Logf ( "Saw pod '%s' in namespace '%s' out of pending state (found '%q')" , podName , ns , pod . Status . Phase )
2015-03-05 20:04:00 +00:00
return true , nil
2015-01-22 16:15:36 +00:00
}
2015-03-05 20:04:00 +00:00
return false , nil
} )
2015-01-22 16:15:36 +00:00
}
2015-03-20 18:26:34 +00:00
// waitForPodSuccessInNamespace returns nil if the pod reached state success, or an error if it reached failure or ran too long.
func waitForPodSuccessInNamespace ( c * client . Client , podName string , contName string , namespace string ) error {
2015-05-21 21:43:42 +00:00
return waitForPodCondition ( c , namespace , podName , "success or failure" , podStartTimeout , func ( pod * api . Pod ) ( bool , error ) {
2015-01-13 02:11:27 +00:00
// Cannot use pod.Status.Phase == api.PodSucceeded/api.PodFailed due to #2632
2015-03-25 11:09:35 +00:00
ci , ok := api . GetContainerStatus ( pod . Status . ContainerStatuses , contName )
2015-01-13 02:11:27 +00:00
if ! ok {
2015-05-18 20:49:32 +00:00
Logf ( "No Status.Info for container '%s' in pod '%s' yet" , contName , podName )
2015-01-13 02:11:27 +00:00
} else {
2015-05-27 22:02:11 +00:00
if ci . State . Terminated != nil {
if ci . State . Terminated . ExitCode == 0 {
2015-02-09 15:48:07 +00:00
By ( "Saw pod success" )
2015-03-05 20:04:00 +00:00
return true , nil
2015-01-13 02:11:27 +00:00
} else {
2015-05-27 22:02:11 +00:00
return true , fmt . Errorf ( "pod '%s' terminated with failure: %+v" , podName , ci . State . Terminated )
2015-01-13 02:11:27 +00:00
}
} else {
2015-05-27 22:02:11 +00:00
Logf ( "Nil State.Terminated for container '%s' in pod '%s' in namespace '%s' so far" , contName , podName , namespace )
2015-01-13 02:11:27 +00:00
}
}
2015-03-05 20:04:00 +00:00
return false , nil
} )
2015-01-13 02:11:27 +00:00
}
2015-03-20 18:26:34 +00:00
// waitForPodSuccess returns nil if the pod reached state success, or an error if it reached failure or ran too long.
// The default namespace is used to identify pods.
func waitForPodSuccess ( c * client . Client , podName string , contName string ) error {
return waitForPodSuccessInNamespace ( c , podName , contName , api . NamespaceDefault )
}
2015-06-08 08:52:37 +00:00
// waitForRCPodOnNode returns the pod from the given replication controller (decribed by rcName) which is scheduled on the given node.
// In case of failure or too long waiting time, an error is returned.
func waitForRCPodOnNode ( c * client . Client , ns , rcName , node string ) ( * api . Pod , error ) {
label := labels . SelectorFromSet ( labels . Set ( map [ string ] string { "name" : rcName } ) )
var p * api . Pod = nil
err := wait . Poll ( 10 * time . Second , 5 * time . Minute , func ( ) ( bool , error ) {
Logf ( "Waiting for pod %s to appear on node %s" , rcName , node )
pods , err := c . Pods ( ns ) . List ( label , fields . Everything ( ) )
if err != nil {
return false , err
}
for _ , pod := range pods . Items {
if pod . Spec . NodeName == node {
Logf ( "Pod %s found on node %s" , pod . Name , node )
p = & pod
return true , nil
}
}
return false , nil
} )
return p , err
}
// waitForRCPodOnNode returns nil if the pod from the given replication controller (decribed by rcName) no longer exists.
// In case of failure or too long waiting time, an error is returned.
func waitForRCPodToDisappear ( c * client . Client , ns , rcName , podName string ) error {
label := labels . SelectorFromSet ( labels . Set ( map [ string ] string { "name" : rcName } ) )
return wait . Poll ( 20 * time . Second , 5 * time . Minute , func ( ) ( bool , error ) {
Logf ( "Waiting for pod %s to disappear" , podName )
pods , err := c . Pods ( ns ) . List ( label , fields . Everything ( ) )
if err != nil {
return false , err
}
found := false
for _ , pod := range pods . Items {
if pod . Name == podName {
Logf ( "Pod %s still exists" , podName )
found = true
}
}
if ! found {
Logf ( "Pod %s no longer exists" , podName )
return true , nil
}
return false , nil
} )
}
2015-06-15 14:31:17 +00:00
// waits until the service appears (exists == true), or disappears (exists == false)
func waitForService ( c * client . Client , namespace , name string , exist bool , interval , timeout time . Duration ) error {
return wait . Poll ( interval , timeout , func ( ) ( bool , error ) {
_ , err := c . Services ( namespace ) . Get ( name )
if err != nil {
Logf ( "Get service %s in namespace %s failed (%v)." , name , namespace , err )
return ! exist , nil
} else {
Logf ( "Service %s in namespace %s found." , name , namespace )
return exist , nil
}
} )
}
// waits until the RC appears (exists == true), or disappears (exists == false)
func waitForReplicationController ( c * client . Client , namespace , name string , exist bool , interval , timeout time . Duration ) error {
return wait . Poll ( interval , timeout , func ( ) ( bool , error ) {
_ , err := c . ReplicationControllers ( namespace ) . Get ( name )
if err != nil {
Logf ( "Get ReplicationController %s in namespace %s failed (%v)." , name , namespace , err )
return ! exist , nil
} else {
Logf ( "ReplicationController %s in namespace %s found." , name , namespace )
return exist , nil
}
} )
}
2015-04-23 14:28:16 +00:00
// Context for checking pods responses by issuing GETs to them and verifying if the answer with pod name.
type podResponseChecker struct {
c * client . Client
ns string
label labels . Selector
controllerName string
2015-06-17 07:13:26 +00:00
respondName bool // Whether the pod should respond with its own name.
2015-04-23 14:28:16 +00:00
pods * api . PodList
}
// checkAllResponses issues GETs to all pods in the context and verify they reply with pod name.
func ( r podResponseChecker ) checkAllResponses ( ) ( done bool , err error ) {
successes := 0
currentPods , err := r . c . Pods ( r . ns ) . List ( r . label , fields . Everything ( ) )
Expect ( err ) . NotTo ( HaveOccurred ( ) )
for i , pod := range r . pods . Items {
// Check that the replica list remains unchanged, otherwise we have problems.
if ! isElementOf ( pod . UID , currentPods ) {
return false , fmt . Errorf ( "pod with UID %s is no longer a member of the replica set. Must have been restarted for some reason. Current replica set: %v" , pod . UID , currentPods )
}
body , err := r . c . Get ( ) .
Prefix ( "proxy" ) .
Namespace ( r . ns ) .
Resource ( "pods" ) .
Name ( string ( pod . Name ) ) .
Do ( ) .
Raw ( )
if err != nil {
2015-06-17 07:13:26 +00:00
Logf ( "Controller %s: Failed to GET from replica %d [%s]: %v:" , r . controllerName , i + 1 , pod . Name , err )
2015-04-23 14:28:16 +00:00
continue
}
2015-06-17 07:13:26 +00:00
// The response checker expects the pod's name unless !respondName, in
// which case it just checks for a non-empty response.
got := string ( body )
what := ""
if r . respondName {
what = "expected"
want := pod . Name
if got != want {
Logf ( "Controller %s: Replica %d [%s] expected response %q but got %q" ,
r . controllerName , i + 1 , pod . Name , want , got )
continue
}
} else {
what = "non-empty"
if len ( got ) == 0 {
Logf ( "Controller %s: Replica %d [%s] expected non-empty response" ,
r . controllerName , i + 1 , pod . Name )
continue
}
2015-04-23 14:28:16 +00:00
}
successes ++
2015-06-17 07:13:26 +00:00
Logf ( "Controller %s: Got %s result from replica %d [%s]: %q, %d of %d required successes so far" ,
r . controllerName , what , i + 1 , pod . Name , got , successes , len ( r . pods . Items ) )
2015-04-23 14:28:16 +00:00
}
if successes < len ( r . pods . Items ) {
return false , nil
}
return true , nil
}
2015-01-08 20:41:38 +00:00
func loadConfig ( ) ( * client . Config , error ) {
2015-03-06 22:49:25 +00:00
switch {
2015-03-31 23:36:31 +00:00
case testContext . KubeConfig != "" :
fmt . Printf ( ">>> testContext.KubeConfig: %s\n" , testContext . KubeConfig )
c , err := clientcmd . LoadFromFile ( testContext . KubeConfig )
2015-03-06 22:49:25 +00:00
if err != nil {
2015-03-31 23:36:31 +00:00
return nil , fmt . Errorf ( "error loading KubeConfig: %v" , err . Error ( ) )
}
if testContext . KubeContext != "" {
fmt . Printf ( ">>> testContext.KubeContext: %s\n" , testContext . KubeContext )
c . CurrentContext = testContext . KubeContext
2015-03-06 22:49:25 +00:00
}
2015-05-19 13:59:46 +00:00
return clientcmd . NewDefaultClientConfig ( * c , & clientcmd . ConfigOverrides { ClusterInfo : clientcmdapi . Cluster { Server : testContext . Host } } ) . ClientConfig ( )
2015-03-06 22:49:25 +00:00
default :
2015-05-13 20:54:02 +00:00
return nil , fmt . Errorf ( "KubeConfig must be specified to load client config" )
2015-01-13 02:11:27 +00:00
}
2015-01-08 20:41:38 +00:00
}
func loadClient ( ) ( * client . Client , error ) {
config , err := loadConfig ( )
2015-01-13 02:11:27 +00:00
if err != nil {
2015-03-06 22:49:25 +00:00
return nil , fmt . Errorf ( "error creating client: %v" , err . Error ( ) )
2015-01-13 02:11:27 +00:00
}
2015-01-08 20:41:38 +00:00
c , err := client . New ( config )
2015-01-13 02:11:27 +00:00
if err != nil {
2015-03-06 22:49:25 +00:00
return nil , fmt . Errorf ( "error creating client: %v" , err . Error ( ) )
2015-01-13 02:11:27 +00:00
}
2015-02-09 15:10:02 +00:00
return c , nil
2015-01-13 02:11:27 +00:00
}
2015-01-24 01:06:13 +00:00
2015-04-17 15:22:29 +00:00
// randomSuffix provides a random string to append to pods,services,rcs.
2015-01-24 01:06:13 +00:00
// TODO: Allow service names to have the same form as names
// for pods and replication controllers so we don't
// need to use such a function and can instead
// use the UUID utilty function.
func randomSuffix ( ) string {
r := rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
return strconv . Itoa ( r . Int ( ) % 10000 )
}
2015-02-12 18:37:31 +00:00
func expectNoError ( err error , explain ... interface { } ) {
ExpectWithOffset ( 1 , err ) . NotTo ( HaveOccurred ( ) , explain ... )
}
2015-03-26 20:34:18 +00:00
2015-06-02 21:40:05 +00:00
// Stops everything from filePath from namespace ns and checks if everything maching selectors from the given namespace is correctly stopped.
2015-05-15 09:39:30 +00:00
func cleanup ( filePath string , ns string , selectors ... string ) {
2015-06-02 21:40:05 +00:00
By ( "using stop to clean up resources" )
2015-05-15 09:39:30 +00:00
var nsArg string
if ns != "" {
nsArg = fmt . Sprintf ( "--namespace=%s" , ns )
}
2015-06-02 21:40:05 +00:00
runKubectl ( "stop" , "-f" , filePath , nsArg )
2015-03-26 20:34:18 +00:00
for _ , selector := range selectors {
2015-06-02 21:40:05 +00:00
resources := runKubectl ( "get" , "pods,rc,se" , "-l" , selector , "--no-headers" , nsArg )
2015-03-26 20:34:18 +00:00
if resources != "" {
Failf ( "Resources left running after stop:\n%s" , resources )
}
}
}
// validatorFn is the function which is individual tests will implement.
// we may want it to return more than just an error, at some point.
type validatorFn func ( c * client . Client , podID string ) error
// validateController is a generic mechanism for testing RC's that are running.
// It takes a container name, a test name, and a validator function which is plugged in by a specific test.
// "containername": this is grepped for.
// "containerImage" : this is the name of the image we expect to be launched. Not to confuse w/ images (kitten.jpg) which are validated.
// "testname": which gets bubbled up to the logging/failure messages if errors happen.
// "validator" function: This function is given a podID and a client, and it can do some specific validations that way.
2015-04-30 02:53:09 +00:00
func validateController ( c * client . Client , containerImage string , replicas int , containername string , testname string , validator validatorFn , ns string ) {
2015-04-08 17:22:33 +00:00
getPodsTemplate := "--template={{range.items}}{{.metadata.name}} {{end}}"
2015-03-26 20:34:18 +00:00
// NB: kubectl adds the "exists" function to the standard template functions.
// This lets us check to see if the "running" entry exists for each of the containers
// we care about. Exists will never return an error and it's safe to check a chain of
// things, any one of which may not exist. In the below template, all of info,
// containername, and running might be nil, so the normal index function isn't very
// helpful.
// This template is unit-tested in kubectl, so if you change it, update the unit test.
// You can read about the syntax here: http://golang.org/pkg/text/template/.
2015-04-08 17:22:33 +00:00
getContainerStateTemplate := fmt . Sprintf ( ` --template= {{ if ( exists . "status" "containerStatuses" ) }} {{ range .status .containerStatuses }} {{ if ( and ( eq .name "%s" ) ( exists . "state" "running" ) ) }} true {{ end }} {{ end }} {{ end }} ` , containername )
2015-03-26 20:34:18 +00:00
2015-04-08 17:22:33 +00:00
getImageTemplate := fmt . Sprintf ( ` --template= {{ if ( exists . "status" "containerStatuses" ) }} {{ range .status .containerStatuses }} {{ if eq .name "%s" }} {{ .image }} {{ end }} {{ end }} {{ end }} ` , containername )
2015-03-26 20:34:18 +00:00
By ( fmt . Sprintf ( "waiting for all containers in %s pods to come up." , testname ) ) //testname should be selector
2015-05-19 18:17:32 +00:00
for start := time . Now ( ) ; time . Since ( start ) < podStartTimeout ; time . Sleep ( 5 * time . Second ) {
2015-06-04 18:15:51 +00:00
getPodsOutput := runKubectl ( "get" , "pods" , "-o" , "template" , getPodsTemplate , "--api-version=v1" , "-l" , testname , fmt . Sprintf ( "--namespace=%v" , ns ) )
2015-03-26 20:34:18 +00:00
pods := strings . Fields ( getPodsOutput )
if numPods := len ( pods ) ; numPods != replicas {
By ( fmt . Sprintf ( "Replicas for %s: expected=%d actual=%d" , testname , replicas , numPods ) )
2015-05-19 18:17:32 +00:00
continue
2015-03-26 20:34:18 +00:00
}
var runningPods [ ] string
for _ , podID := range pods {
2015-06-04 18:15:51 +00:00
running := runKubectl ( "get" , "pods" , podID , "-o" , "template" , getContainerStateTemplate , "--api-version=v1" , fmt . Sprintf ( "--namespace=%v" , ns ) )
2015-04-08 17:22:33 +00:00
if running != "true" {
2015-03-26 20:34:18 +00:00
Logf ( "%s is created but not running" , podID )
2015-05-19 18:17:32 +00:00
continue
2015-03-26 20:34:18 +00:00
}
2015-06-04 18:15:51 +00:00
currentImage := runKubectl ( "get" , "pods" , podID , "-o" , "template" , getImageTemplate , "--api-version=v1" , fmt . Sprintf ( "--namespace=%v" , ns ) )
2015-03-26 20:34:18 +00:00
if currentImage != containerImage {
Logf ( "%s is created but running wrong image; expected: %s, actual: %s" , podID , containerImage , currentImage )
2015-05-19 18:17:32 +00:00
continue
2015-03-26 20:34:18 +00:00
}
// Call the generic validator function here.
// This might validate for example, that (1) getting a url works and (2) url is serving correct content.
if err := validator ( c , podID ) ; err != nil {
Logf ( "%s is running right image but validator function failed: %v" , podID , err )
2015-05-19 18:17:32 +00:00
continue
2015-03-26 20:34:18 +00:00
}
Logf ( "%s is verified up and running" , podID )
runningPods = append ( runningPods , podID )
}
// If we reach here, then all our checks passed.
if len ( runningPods ) == replicas {
2015-05-19 18:17:32 +00:00
return
2015-03-26 20:34:18 +00:00
}
2015-05-19 18:17:32 +00:00
}
2015-03-26 20:34:18 +00:00
// Reaching here means that one of more checks failed multiple times. Assuming its not a race condition, something is broken.
Failf ( "Timed out after %v seconds waiting for %s pods to reach valid state" , podStartTimeout . Seconds ( ) , testname )
}
2015-05-19 16:13:08 +00:00
// kubectlCmd runs the kubectl executable through the wrapper script.
2015-03-26 20:34:18 +00:00
func kubectlCmd ( args ... string ) * exec . Cmd {
defaultArgs := [ ] string { }
2015-04-21 14:07:08 +00:00
// Reference a --server option so tests can run anywhere.
if testContext . Host != "" {
defaultArgs = append ( defaultArgs , "--" + clientcmd . FlagAPIServer + "=" + testContext . Host )
}
2015-03-31 23:36:31 +00:00
if testContext . KubeConfig != "" {
defaultArgs = append ( defaultArgs , "--" + clientcmd . RecommendedConfigPathFlag + "=" + testContext . KubeConfig )
2015-04-20 19:51:16 +00:00
// Reference the KubeContext
2015-04-02 04:24:16 +00:00
if testContext . KubeContext != "" {
defaultArgs = append ( defaultArgs , "--" + clientcmd . FlagContext + "=" + testContext . KubeContext )
}
2015-04-20 19:51:16 +00:00
2015-03-26 20:34:18 +00:00
} else {
2015-03-31 23:36:31 +00:00
if testContext . CertDir != "" {
2015-03-26 20:34:18 +00:00
defaultArgs = append ( defaultArgs ,
2015-03-31 23:36:31 +00:00
fmt . Sprintf ( "--certificate-authority=%s" , filepath . Join ( testContext . CertDir , "ca.crt" ) ) ,
fmt . Sprintf ( "--client-certificate=%s" , filepath . Join ( testContext . CertDir , "kubecfg.crt" ) ) ,
fmt . Sprintf ( "--client-key=%s" , filepath . Join ( testContext . CertDir , "kubecfg.key" ) ) )
2015-03-26 20:34:18 +00:00
}
}
kubectlArgs := append ( defaultArgs , args ... )
2015-04-02 04:24:16 +00:00
2015-05-26 16:27:13 +00:00
//We allow users to specify path to kubectl, so you can test either "kubectl" or "cluster/kubectl.sh"
//and so on.
cmd := exec . Command ( testContext . KubectlPath , kubectlArgs ... )
//caller will invoke this and wait on it.
2015-03-26 20:34:18 +00:00
return cmd
}
func runKubectl ( args ... string ) string {
var stdout , stderr bytes . Buffer
cmd := kubectlCmd ( args ... )
cmd . Stdout , cmd . Stderr = & stdout , & stderr
2015-05-26 16:27:13 +00:00
Logf ( "Running '%s %s'" , cmd . Path , strings . Join ( cmd . Args , " " ) )
2015-03-26 20:34:18 +00:00
if err := cmd . Run ( ) ; err != nil {
Failf ( "Error running %v:\nCommand stdout:\n%v\nstderr:\n%v\n" , cmd , cmd . Stdout , cmd . Stderr )
return ""
}
Logf ( stdout . String ( ) )
// TODO: trimspace should be unnecessary after switching to use kubectl binary directly
return strings . TrimSpace ( stdout . String ( ) )
}
2015-03-31 17:25:14 +00:00
// testContainerOutput runs testContainerOutputInNamespace with the default namespace.
func testContainerOutput ( scenarioName string , c * client . Client , pod * api . Pod , expectedOutput [ ] string ) {
2015-04-27 13:35:28 +00:00
testContainerOutputInNamespace ( scenarioName , c , pod , expectedOutput , api . NamespaceDefault )
2015-03-31 17:25:14 +00:00
}
// testContainerOutputInNamespace runs the given pod in the given namespace and waits
// for the first container in the podSpec to move into the 'Success' status. It retrieves
// the container log and searches for lines of expected output.
2015-04-27 13:35:28 +00:00
func testContainerOutputInNamespace ( scenarioName string , c * client . Client , pod * api . Pod , expectedOutput [ ] string , ns string ) {
2015-03-31 17:25:14 +00:00
By ( fmt . Sprintf ( "Creating a pod to test %v" , scenarioName ) )
2015-04-28 12:21:57 +00:00
defer c . Pods ( ns ) . Delete ( pod . Name , nil )
2015-03-31 17:25:14 +00:00
if _ , err := c . Pods ( ns ) . Create ( pod ) ; err != nil {
Failf ( "Failed to create pod: %v" , err )
}
containerName := pod . Spec . Containers [ 0 ] . Name
// Wait for client pod to complete.
2015-04-26 22:54:10 +00:00
expectNoError ( waitForPodSuccessInNamespace ( c , pod . Name , containerName , ns ) )
2015-03-31 17:25:14 +00:00
// Grab its logs. Get host first.
podStatus , err := c . Pods ( ns ) . Get ( pod . Name )
if err != nil {
Failf ( "Failed to get pod status: %v" , err )
}
2015-05-22 23:40:57 +00:00
By ( fmt . Sprintf ( "Trying to get logs from node %s pod %s container %s: %v" ,
podStatus . Spec . NodeName , podStatus . Name , containerName , err ) )
2015-03-31 17:25:14 +00:00
var logs [ ] byte
2015-05-19 18:17:32 +00:00
start := time . Now ( )
2015-03-31 17:25:14 +00:00
// Sometimes the actual containers take a second to get started, try to get logs for 60s
2015-05-19 18:17:32 +00:00
for time . Now ( ) . Sub ( start ) < ( 60 * time . Second ) {
2015-03-31 17:25:14 +00:00
logs , err = c . Get ( ) .
Prefix ( "proxy" ) .
2015-04-02 20:15:39 +00:00
Resource ( "nodes" ) .
2015-05-22 23:40:57 +00:00
Name ( podStatus . Spec . NodeName ) .
2015-03-31 17:25:14 +00:00
Suffix ( "containerLogs" , ns , podStatus . Name , containerName ) .
Do ( ) .
Raw ( )
2015-05-19 18:17:32 +00:00
fmt . Sprintf ( "pod logs:%v\n" , string ( logs ) )
By ( fmt . Sprintf ( "pod logs:%v\n" , string ( logs ) ) )
2015-03-31 17:25:14 +00:00
if strings . Contains ( string ( logs ) , "Internal Error" ) {
2015-05-22 23:40:57 +00:00
By ( fmt . Sprintf ( "Failed to get logs from node %q pod %q container %q: %v" ,
podStatus . Spec . NodeName , podStatus . Name , containerName , string ( logs ) ) )
2015-05-19 18:17:32 +00:00
time . Sleep ( 5 * time . Second )
continue
2015-03-31 17:25:14 +00:00
}
2015-05-19 18:17:32 +00:00
break
}
2015-03-31 17:25:14 +00:00
for _ , m := range expectedOutput {
Expect ( string ( logs ) ) . To ( ContainSubstring ( m ) , "%q in container output" , m )
}
}
2015-04-22 19:08:08 +00:00
2015-05-08 00:11:48 +00:00
// podInfo contains pod information useful for debugging e2e tests.
type podInfo struct {
oldHostname string
oldPhase string
hostname string
phase string
}
// PodDiff is a map of pod name to podInfos
type PodDiff map [ string ] * podInfo
// Print formats and prints the give PodDiff.
func ( p PodDiff ) Print ( ignorePhases util . StringSet ) {
for name , info := range p {
if ignorePhases . Has ( info . phase ) {
continue
}
if info . phase == nonExist {
2015-06-12 14:15:29 +00:00
Logf ( "Pod %v was deleted, had phase %v and host %v" , name , info . oldPhase , info . oldHostname )
2015-05-08 00:11:48 +00:00
continue
}
phaseChange , hostChange := false , false
msg := fmt . Sprintf ( "Pod %v " , name )
if info . oldPhase != info . phase {
phaseChange = true
if info . oldPhase == nonExist {
msg += fmt . Sprintf ( "in phase %v " , info . phase )
} else {
msg += fmt . Sprintf ( "went from phase: %v -> %v " , info . oldPhase , info . phase )
}
}
if info . oldHostname != info . hostname {
hostChange = true
if info . oldHostname == nonExist || info . oldHostname == "" {
msg += fmt . Sprintf ( "assigned host %v " , info . hostname )
} else {
msg += fmt . Sprintf ( "went from host: %v -> %v " , info . oldHostname , info . hostname )
}
}
if phaseChange || hostChange {
Logf ( msg )
}
}
}
// Diff computes a PodDiff given 2 lists of pods.
2015-06-01 12:24:44 +00:00
func Diff ( oldPods [ ] * api . Pod , curPods [ ] * api . Pod ) PodDiff {
2015-05-08 00:11:48 +00:00
podInfoMap := PodDiff { }
// New pods will show up in the curPods list but not in oldPods. They have oldhostname/phase == nonexist.
2015-06-01 12:24:44 +00:00
for _ , pod := range curPods {
2015-05-22 23:40:57 +00:00
podInfoMap [ pod . Name ] = & podInfo { hostname : pod . Spec . NodeName , phase : string ( pod . Status . Phase ) , oldHostname : nonExist , oldPhase : nonExist }
2015-05-08 00:11:48 +00:00
}
// Deleted pods will show up in the oldPods list but not in curPods. They have a hostname/phase == nonexist.
2015-06-01 12:24:44 +00:00
for _ , pod := range oldPods {
2015-05-08 00:11:48 +00:00
if info , ok := podInfoMap [ pod . Name ] ; ok {
2015-05-22 23:40:57 +00:00
info . oldHostname , info . oldPhase = pod . Spec . NodeName , string ( pod . Status . Phase )
2015-05-08 00:11:48 +00:00
} else {
2015-05-22 23:40:57 +00:00
podInfoMap [ pod . Name ] = & podInfo { hostname : nonExist , phase : nonExist , oldHostname : pod . Spec . NodeName , oldPhase : string ( pod . Status . Phase ) }
2015-05-08 00:11:48 +00:00
}
}
return podInfoMap
}
2015-05-06 15:03:22 +00:00
// RunRC Launches (and verifies correctness) of a Replication Controller
2015-06-01 18:27:14 +00:00
// and will wait for all pods it spawns to become "Running".
2015-05-06 15:03:22 +00:00
// It's the caller's responsibility to clean up externally (i.e. use the
// namespace lifecycle for handling cleanup).
2015-05-26 14:24:46 +00:00
func RunRC ( config RCConfig ) error {
2015-06-10 11:59:30 +00:00
maxContainerFailures := int ( math . Max ( 1.0 , float64 ( config . Replicas ) * .01 ) )
label := labels . SelectorFromSet ( labels . Set ( map [ string ] string { "name" : config . Name } ) )
2015-06-03 18:59:29 +00:00
2015-06-10 11:59:30 +00:00
By ( fmt . Sprintf ( "%v Creating replication controller %s" , time . Now ( ) , config . Name ) )
2015-05-08 00:11:48 +00:00
rc := & api . ReplicationController {
2015-04-22 19:08:08 +00:00
ObjectMeta : api . ObjectMeta {
2015-06-10 11:59:30 +00:00
Name : config . Name ,
2015-04-22 19:08:08 +00:00
} ,
Spec : api . ReplicationControllerSpec {
2015-06-10 11:59:30 +00:00
Replicas : config . Replicas ,
2015-04-22 19:08:08 +00:00
Selector : map [ string ] string {
2015-06-10 11:59:30 +00:00
"name" : config . Name ,
2015-04-22 19:08:08 +00:00
} ,
Template : & api . PodTemplateSpec {
ObjectMeta : api . ObjectMeta {
2015-06-10 11:59:30 +00:00
Labels : map [ string ] string { "name" : config . Name } ,
2015-04-22 19:08:08 +00:00
} ,
Spec : api . PodSpec {
Containers : [ ] api . Container {
{
2015-06-10 11:59:30 +00:00
Name : config . Name ,
Image : config . Image ,
2015-04-22 19:08:08 +00:00
Ports : [ ] api . ContainerPort { { ContainerPort : 80 } } ,
} ,
} ,
} ,
} ,
} ,
2015-05-08 00:11:48 +00:00
}
2015-06-11 22:55:25 +00:00
if config . Env != nil {
for k , v := range config . Env {
c := & rc . Spec . Template . Spec . Containers [ 0 ]
c . Env = append ( c . Env , api . EnvVar { Name : k , Value : v } )
}
}
if config . Labels != nil {
for k , v := range config . Labels {
rc . Spec . Template . ObjectMeta . Labels [ k ] = v
}
}
if config . Ports != nil {
for k , v := range config . Ports {
c := & rc . Spec . Template . Spec . Containers [ 0 ]
c . Ports = append ( c . Ports , api . ContainerPort { Name : k , ContainerPort : v } )
}
}
2015-06-10 11:59:30 +00:00
_ , err := config . Client . ReplicationControllers ( config . Namespace ) . Create ( rc )
2015-04-22 19:08:08 +00:00
if err != nil {
return fmt . Errorf ( "Error creating replication controller: %v" , err )
}
2015-06-10 11:59:30 +00:00
Logf ( "%v Created replication controller with name: %v, namespace: %v, replica count: %v" , time . Now ( ) , rc . Name , config . Namespace , rc . Spec . Replicas )
podStore := newPodStore ( config . Client , config . Namespace , label , fields . Everything ( ) )
2015-06-01 12:24:44 +00:00
defer podStore . Stop ( )
2015-04-22 19:08:08 +00:00
2015-06-10 11:59:30 +00:00
interval := config . PollInterval
if interval <= 0 {
interval = 10 * time . Second
}
oldPods := make ( [ ] * api . Pod , 0 )
oldRunning := 0
lastChange := time . Now ( )
for oldRunning != config . Replicas && time . Since ( lastChange ) < 5 * time . Minute {
time . Sleep ( interval )
running := 0
waiting := 0
pending := 0
unknown := 0
inactive := 0
failedContainers := 0
pods := podStore . List ( )
2015-06-11 22:55:25 +00:00
if config . CreatedPods != nil {
* config . CreatedPods = pods
}
2015-06-10 11:59:30 +00:00
for _ , p := range pods {
if p . Status . Phase == api . PodRunning {
running ++
for _ , v := range FailedContainers ( p ) {
failedContainers = failedContainers + v . restarts
}
} else if p . Status . Phase == api . PodPending {
if p . Spec . NodeName == "" {
waiting ++
} else {
pending ++
}
} else if p . Status . Phase == api . PodSucceeded || p . Status . Phase == api . PodFailed {
inactive ++
} else if p . Status . Phase == api . PodUnknown {
unknown ++
2015-05-26 14:24:46 +00:00
}
2015-05-22 20:00:46 +00:00
}
2015-05-26 14:24:46 +00:00
2015-06-10 11:59:30 +00:00
Logf ( "%v Pods: %d out of %d created, %d running, %d pending, %d waiting, %d inactive, %d unknown " ,
time . Now ( ) , len ( pods ) , config . Replicas , running , pending , waiting , inactive , unknown )
if config . PodStatusFile != nil {
fmt . Fprintf ( config . PodStatusFile , "%s, %d, running, %d, pending, %d, waiting, %d, inactive, %d, unknown\n" , time . Now ( ) , running , pending , waiting , inactive , unknown )
2015-05-26 14:24:46 +00:00
}
2015-06-10 11:59:30 +00:00
if failedContainers > maxContainerFailures {
return fmt . Errorf ( "%d containers failed which is more than allowed %d" , failedContainers , maxContainerFailures )
}
if len ( pods ) < len ( oldPods ) || len ( pods ) > config . Replicas {
// This failure mode includes:
// kubelet is dead, so node controller deleted pods and rc creates more
// - diagnose by noting the pod diff below.
// pod is unhealthy, so replication controller creates another to take its place
// - diagnose by comparing the previous "2 Pod states" lines for inactive pods
errorStr := fmt . Sprintf ( "Number of reported pods changed: %d vs %d" , len ( pods ) , len ( oldPods ) )
Logf ( "%v, pods that changed since the last iteration:" , errorStr )
Diff ( oldPods , pods ) . Print ( util . NewStringSet ( ) )
return fmt . Errorf ( errorStr )
}
2015-05-26 14:24:46 +00:00
2015-06-10 11:59:30 +00:00
if len ( pods ) > len ( oldPods ) || running > oldRunning {
lastChange = time . Now ( )
2015-05-14 13:18:24 +00:00
}
2015-06-10 11:59:30 +00:00
oldPods = pods
oldRunning = running
2015-04-22 19:08:08 +00:00
}
2015-06-10 11:59:30 +00:00
if oldRunning != config . Replicas {
return fmt . Errorf ( "Only %d pods started out of %d" , oldRunning , config . Replicas )
2015-04-22 19:08:08 +00:00
}
return nil
}
2015-05-21 21:10:25 +00:00
func ScaleRC ( c * client . Client , ns , name string , size uint ) error {
2015-06-09 14:13:02 +00:00
By ( fmt . Sprintf ( "%v Scaling replication controller %s in namespace %s to %d" , time . Now ( ) , name , ns , size ) )
2015-05-21 21:10:25 +00:00
scaler , err := kubectl . ScalerFor ( "ReplicationController" , kubectl . NewScalerClient ( c ) )
2015-05-05 14:48:50 +00:00
if err != nil {
return err
}
waitForReplicas := kubectl . NewRetryParams ( 5 * time . Second , 5 * time . Minute )
2015-05-21 21:10:25 +00:00
if err = scaler . Scale ( ns , name , size , nil , nil , waitForReplicas ) ; err != nil {
2015-05-05 14:48:50 +00:00
return err
}
return waitForRCPodsRunning ( c , ns , name )
}
// Wait up to 10 minutes for pods to become Running.
func waitForRCPodsRunning ( c * client . Client , ns , rcName string ) error {
running := false
label := labels . SelectorFromSet ( labels . Set ( map [ string ] string { "name" : rcName } ) )
2015-06-01 12:24:44 +00:00
podStore := newPodStore ( c , ns , label , fields . Everything ( ) )
defer podStore . Stop ( )
2015-05-05 14:48:50 +00:00
for start := time . Now ( ) ; time . Since ( start ) < 10 * time . Minute ; time . Sleep ( 5 * time . Second ) {
2015-06-01 12:24:44 +00:00
pods := podStore . List ( )
for _ , p := range pods {
2015-05-05 14:48:50 +00:00
if p . Status . Phase != api . PodRunning {
continue
}
}
running = true
break
}
if ! running {
return fmt . Errorf ( "Timeout while waiting for replication controller %s pods to be running" , rcName )
}
return nil
}
// Delete a Replication Controller and all pods it spawned
func DeleteRC ( c * client . Client , ns , name string ) error {
2015-06-09 14:13:02 +00:00
By ( fmt . Sprintf ( "%v Deleting replication controller %s in namespace %s" , time . Now ( ) , name , ns ) )
2015-05-25 08:20:34 +00:00
reaper , err := kubectl . ReaperForReplicationController ( c , 10 * time . Minute )
2015-05-05 14:48:50 +00:00
if err != nil {
return err
}
2015-05-25 08:20:34 +00:00
startTime := time . Now ( )
2015-05-29 23:16:30 +00:00
_ , err = reaper . Stop ( ns , name , 0 , api . NewDeleteOptions ( 0 ) )
2015-05-25 08:20:34 +00:00
deleteRCTime := time . Now ( ) . Sub ( startTime )
Logf ( "Deleting RC took: %v" , deleteRCTime )
2015-05-05 14:48:50 +00:00
return err
}
2015-05-21 21:43:42 +00:00
// Convenient wrapper around listing nodes supporting retries.
func listNodes ( c * client . Client , label labels . Selector , field fields . Selector ) ( * api . NodeList , error ) {
var nodes * api . NodeList
var errLast error
if wait . Poll ( poll , singleCallTimeout , func ( ) ( bool , error ) {
nodes , errLast = c . Nodes ( ) . List ( label , field )
return errLast == nil , nil
} ) != nil {
return nil , fmt . Errorf ( "listNodes() failed with last error: %v" , errLast )
}
return nodes , nil
}
2015-05-14 13:18:24 +00:00
// FailedContainers inspects all containers in a pod and returns failure
// information for containers that have failed or been restarted.
// A map is returned where the key is the containerID and the value is a
// struct containing the restart and failure information
2015-05-26 14:24:46 +00:00
func FailedContainers ( pod * api . Pod ) map [ string ] ContainerFailures {
2015-05-14 13:18:24 +00:00
var state ContainerFailures
states := make ( map [ string ] ContainerFailures )
2015-04-27 18:05:41 +00:00
statuses := pod . Status . ContainerStatuses
if len ( statuses ) == 0 {
return nil
} else {
for _ , status := range statuses {
2015-05-27 22:02:11 +00:00
if status . State . Terminated != nil {
states [ status . ContainerID ] = ContainerFailures { status : status . State . Terminated }
} else if status . LastTerminationState . Terminated != nil {
states [ status . ContainerID ] = ContainerFailures { status : status . LastTerminationState . Terminated }
2015-05-06 15:03:22 +00:00
}
2015-05-14 13:18:24 +00:00
if status . RestartCount > 0 {
var ok bool
if state , ok = states [ status . ContainerID ] ; ! ok {
state = ContainerFailures { }
}
state . restarts = status . RestartCount
states [ status . ContainerID ] = state
2015-04-27 18:05:41 +00:00
}
}
}
2015-05-14 13:18:24 +00:00
return states
2015-04-27 18:05:41 +00:00
}
2015-04-28 11:58:20 +00:00
// Prints the histogram of the events and returns the number of bad events.
func BadEvents ( events [ ] * api . Event ) int {
type histogramKey struct {
reason string
source string
}
histogram := make ( map [ histogramKey ] int )
for _ , e := range events {
histogram [ histogramKey { reason : e . Reason , source : e . Source . Component } ] ++
}
for key , number := range histogram {
Logf ( "- reason: %s, source: %s -> %d" , key . reason , key . source , number )
}
badPatterns := [ ] string { "kill" , "fail" }
badEvents := 0
for key , number := range histogram {
for _ , s := range badPatterns {
if strings . Contains ( key . reason , s ) {
Logf ( "WARNING %d events from %s with reason: %s" , number , key . source , key . reason )
badEvents += number
break
}
}
}
return badEvents
}
2015-04-24 17:26:12 +00:00
2015-04-29 22:28:48 +00:00
// NodeSSHHosts returns SSH-able host names for all nodes. It returns an error
// if it can't find an external IP for every node, though it still returns all
// hosts that it found in that case.
func NodeSSHHosts ( c * client . Client ) ( [ ] string , error ) {
var hosts [ ] string
nodelist , err := c . Nodes ( ) . List ( labels . Everything ( ) , fields . Everything ( ) )
if err != nil {
return hosts , fmt . Errorf ( "error getting nodes: %v" , err )
}
for _ , n := range nodelist . Items {
for _ , addr := range n . Status . Addresses {
// Use the first external IP address we find on the node, and
// use at most one per node.
// TODO(mbforbes): Use the "preferred" address for the node, once
// such a thing is defined (#2462).
if addr . Type == api . NodeExternalIP {
hosts = append ( hosts , addr . Address + ":22" )
break
}
}
}
// Error if any node didn't have an external IP.
if len ( hosts ) != len ( nodelist . Items ) {
return hosts , fmt . Errorf (
"only found %d external IPs on nodes, but found %d nodes. Nodelist: %v" ,
len ( hosts ) , len ( nodelist . Items ) , nodelist )
}
return hosts , nil
}
2015-04-24 17:26:12 +00:00
// SSH synchronously SSHs to a node running on provider and runs cmd. If there
// is no error performing the SSH, the stdout, stderr, and exit code are
// returned.
func SSH ( cmd , host , provider string ) ( string , string , int , error ) {
// Get a signer for the provider.
signer , err := getSigner ( provider )
if err != nil {
return "" , "" , 0 , fmt . Errorf ( "error getting signer for provider %s: '%v'" , provider , err )
}
2015-06-16 11:12:25 +00:00
user := os . Getenv ( "KUBE_SSH_USER" )
// RunSSHCommand will default to Getenv("USER") if user == ""
return util . RunSSHCommand ( cmd , user , host , signer )
2015-04-24 17:26:12 +00:00
}
// getSigner returns an ssh.Signer for the provider ("gce", etc.) that can be
// used to SSH to their nodes.
func getSigner ( provider string ) ( ssh . Signer , error ) {
// Get the directory in which SSH keys are located.
keydir := filepath . Join ( os . Getenv ( "HOME" ) , ".ssh" )
// Select the key itself to use. When implementing more providers here,
// please also add them to any SSH tests that are disabled because of signer
// support.
keyfile := ""
switch provider {
case "gce" , "gke" :
keyfile = "google_compute_engine"
2015-06-13 21:45:03 +00:00
case "aws" :
keyfile = "kube_aws_rsa"
2015-04-24 17:26:12 +00:00
default :
return nil , fmt . Errorf ( "getSigner(...) not implemented for %s" , provider )
}
key := filepath . Join ( keydir , keyfile )
2015-05-04 20:55:05 +00:00
Logf ( "Using SSH key: %s" , key )
2015-04-24 17:26:12 +00:00
2015-06-08 22:45:20 +00:00
return util . MakePrivateKeySignerFromFile ( key )
2015-04-24 17:26:12 +00:00
}
2015-04-29 10:24:01 +00:00
2015-05-21 21:43:42 +00:00
// checkPodsRunning returns whether all pods whose names are listed in podNames
2015-06-17 07:13:26 +00:00
// in namespace ns are running and ready, using c and waiting at most timeout.
func checkPodsRunningReady ( c * client . Client , ns string , podNames [ ] string , timeout time . Duration ) bool {
2015-05-21 21:43:42 +00:00
np , desc := len ( podNames ) , "running and ready"
Logf ( "Waiting up to %v for the following %d pods to be %s: %s" , timeout , np , desc , podNames )
result := make ( chan bool , len ( podNames ) )
for ix := range podNames {
// Launch off pod readiness checkers.
go func ( name string ) {
2015-06-17 07:13:26 +00:00
err := waitForPodCondition ( c , ns , name , desc , timeout , podRunningReady )
2015-05-21 21:43:42 +00:00
result <- err == nil
} ( podNames [ ix ] )
}
// Wait for them all to finish.
success := true
// TODO(mbforbes): Change to `for range` syntax and remove logging once we
// support only Go >= 1.4.
for _ , podName := range podNames {
if ! <- result {
Logf ( "Pod %-[1]*[2]s failed to be %[3]s." , podPrintWidth , podName , desc )
success = false
}
}
Logf ( "Wanted all %d pods to be %s. Result: %t. Pods: %v" , np , desc , success , podNames )
return success
}
// waitForNodeToBeReady returns whether node name is ready within timeout.
func waitForNodeToBeReady ( c * client . Client , name string , timeout time . Duration ) bool {
return waitForNodeToBe ( c , name , true , timeout )
}
// waitForNodeToBeNotReady returns whether node name is not ready (i.e. the
// readiness condition is anything but ready, e.g false or unknown) within
// timeout.
func waitForNodeToBeNotReady ( c * client . Client , name string , timeout time . Duration ) bool {
return waitForNodeToBe ( c , name , false , timeout )
}
2015-06-11 17:29:41 +00:00
func isNodeReadySetAsExpected ( node * api . Node , wantReady bool ) bool {
// Check the node readiness condition (logging all).
for i , cond := range node . Status . Conditions {
Logf ( "Node %s condition %d/%d: type: %v, status: %v" ,
node . Name , i + 1 , len ( node . Status . Conditions ) , cond . Type , cond . Status )
// Ensure that the condition type is readiness and the status
// matches as desired.
if cond . Type == api . NodeReady && ( cond . Status == api . ConditionTrue ) == wantReady {
Logf ( "Successfully found node %s readiness to be %t" , node . Name , wantReady )
return true
}
}
return false
}
2015-05-21 21:43:42 +00:00
// waitForNodeToBe returns whether node name's readiness state matches wantReady
// within timeout. If wantReady is true, it will ensure the node is ready; if
// it's false, it ensures the node is in any state other than ready (e.g. not
// ready or unknown).
func waitForNodeToBe ( c * client . Client , name string , wantReady bool , timeout time . Duration ) bool {
Logf ( "Waiting up to %v for node %s readiness to be %t" , timeout , name , wantReady )
for start := time . Now ( ) ; time . Since ( start ) < timeout ; time . Sleep ( poll ) {
node , err := c . Nodes ( ) . Get ( name )
if err != nil {
Logf ( "Couldn't get node %s" , name )
continue
}
2015-06-11 17:29:41 +00:00
if isNodeReadySetAsExpected ( node , wantReady ) {
return true
2015-05-21 21:43:42 +00:00
}
}
Logf ( "Node %s didn't reach desired readiness (%t) within %v" , name , wantReady , timeout )
return false
}
2015-06-11 17:29:41 +00:00
// Filters nodes in NodeList in place, removing nodes that do not
// satisfy the given condition
// TODO: consider merging with pkg/client/cache.NodeLister
func filterNodes ( nodeList * api . NodeList , fn func ( node api . Node ) bool ) {
var l [ ] api . Node
for _ , node := range nodeList . Items {
if fn ( node ) {
l = append ( l , node )
}
}
nodeList . Items = l
}
2015-04-29 10:24:01 +00:00
// LatencyMetrics stores data about request latency at a given quantile
// broken down by verb (e.g. GET, PUT, LIST) and resource (e.g. pods, services).
type LatencyMetric struct {
2015-05-25 09:43:26 +00:00
Verb string
Resource string
2015-04-29 10:24:01 +00:00
// 0 <= quantile <=1, e.g. 0.95 is 95%tile, 0.5 is median.
2015-05-25 09:43:26 +00:00
Quantile float64
Latency time . Duration
2015-04-29 10:24:01 +00:00
}
2015-05-25 09:43:26 +00:00
// LatencyMetricByLatency implements sort.Interface for []LatencyMetric based on
// the latency field.
type LatencyMetricByLatency [ ] LatencyMetric
func ( a LatencyMetricByLatency ) Len ( ) int { return len ( a ) }
func ( a LatencyMetricByLatency ) Swap ( i , j int ) { a [ i ] , a [ j ] = a [ j ] , a [ i ] }
func ( a LatencyMetricByLatency ) Less ( i , j int ) bool { return a [ i ] . Latency < a [ j ] . Latency }
2015-04-29 10:24:01 +00:00
func ReadLatencyMetrics ( c * client . Client ) ( [ ] LatencyMetric , error ) {
2015-05-26 14:24:46 +00:00
body , err := getMetrics ( c )
2015-04-29 10:24:01 +00:00
if err != nil {
return nil , err
}
metrics := make ( [ ] LatencyMetric , 0 )
for _ , line := range strings . Split ( string ( body ) , "\n" ) {
if strings . HasPrefix ( line , "apiserver_request_latencies_summary{" ) {
// Example line:
// apiserver_request_latencies_summary{resource="namespaces",verb="LIST",quantile="0.99"} 908
// TODO: This parsing code is long and not readable. We should improve it.
keyVal := strings . Split ( line , " " )
if len ( keyVal ) != 2 {
return nil , fmt . Errorf ( "Error parsing metric %q" , line )
}
keyElems := strings . Split ( line , "\"" )
if len ( keyElems ) != 7 {
return nil , fmt . Errorf ( "Error parsing metric %q" , line )
}
resource := keyElems [ 1 ]
verb := keyElems [ 3 ]
quantile , err := strconv . ParseFloat ( keyElems [ 5 ] , 64 )
if err != nil {
return nil , fmt . Errorf ( "Error parsing metric %q" , line )
}
latency , err := strconv . ParseFloat ( keyVal [ 1 ] , 64 )
if err != nil {
return nil , fmt . Errorf ( "Error parsing metric %q" , line )
}
metrics = append ( metrics , LatencyMetric { verb , resource , quantile , time . Duration ( int64 ( latency ) ) * time . Microsecond } )
}
}
return metrics , nil
}
// Prints summary metrics for request types with latency above threshold
// and returns number of such request types.
2015-05-06 20:50:36 +00:00
func HighLatencyRequests ( c * client . Client , threshold time . Duration , ignoredResources util . StringSet ) ( int , error ) {
2015-05-25 07:21:26 +00:00
ignoredVerbs := util . NewStringSet ( "WATCHLIST" , "PROXY" )
2015-04-29 10:24:01 +00:00
metrics , err := ReadLatencyMetrics ( c )
if err != nil {
return 0 , err
}
2015-05-25 09:43:26 +00:00
sort . Sort ( sort . Reverse ( LatencyMetricByLatency ( metrics ) ) )
2015-04-29 10:24:01 +00:00
var badMetrics [ ] LatencyMetric
2015-05-25 09:43:26 +00:00
top := 5
2015-04-29 10:24:01 +00:00
for _ , metric := range metrics {
2015-05-25 09:43:26 +00:00
if ignoredResources . Has ( metric . Resource ) || ignoredVerbs . Has ( metric . Verb ) {
continue
}
isBad := false
if metric . Latency > threshold &&
2015-04-29 10:24:01 +00:00
// We are only interested in 99%tile, but for logging purposes
// it's useful to have all the offending percentiles.
2015-05-25 09:43:26 +00:00
metric . Quantile <= 0.99 {
2015-04-29 10:24:01 +00:00
badMetrics = append ( badMetrics , metric )
2015-05-25 09:43:26 +00:00
isBad = true
}
if top > 0 || isBad {
top --
prefix := ""
if isBad {
prefix = "WARNING "
}
Logf ( "%vTop latency metric: %+v" , prefix , metric )
2015-04-29 10:24:01 +00:00
}
}
return len ( badMetrics ) , nil
}
2015-05-26 14:24:46 +00:00
// Retrieve metrics information
func getMetrics ( c * client . Client ) ( string , error ) {
body , err := c . Get ( ) . AbsPath ( "/metrics" ) . DoRaw ( )
if err != nil {
return "" , err
}
return string ( body ) , nil
}
// Retrieve debug information
func getDebugInfo ( c * client . Client ) ( map [ string ] string , error ) {
data := make ( map [ string ] string )
for _ , key := range [ ] string { "block" , "goroutine" , "heap" , "threadcreate" } {
resp , err := http . Get ( c . Get ( ) . AbsPath ( fmt . Sprintf ( "debug/pprof/%s" , key ) ) . URL ( ) . String ( ) + "?debug=2" )
2015-05-26 15:08:17 +00:00
if err != nil {
Logf ( "Warning: Error trying to fetch %s debug data: %v" , key , err )
continue
}
2015-05-26 14:24:46 +00:00
body , err := ioutil . ReadAll ( resp . Body )
resp . Body . Close ( )
if err != nil {
2015-05-26 15:08:17 +00:00
Logf ( "Warning: Error trying to read %s debug data: %v" , key , err )
2015-05-26 14:24:46 +00:00
}
data [ key ] = string ( body )
}
return data , nil
}
func writePerfData ( c * client . Client , dirName string , postfix string ) error {
fname := fmt . Sprintf ( "%s/metrics_%s.txt" , dirName , postfix )
handler , err := os . Create ( fname )
if err != nil {
return fmt . Errorf ( "Error creating file '%s': %v" , fname , err )
}
metrics , err := getMetrics ( c )
if err != nil {
return fmt . Errorf ( "Error retrieving metrics: %v" , err )
}
_ , err = handler . WriteString ( metrics )
if err != nil {
return fmt . Errorf ( "Error writing metrics: %v" , err )
}
err = handler . Close ( )
if err != nil {
return fmt . Errorf ( "Error closing '%s': %v" , fname , err )
}
debug , err := getDebugInfo ( c )
if err != nil {
return fmt . Errorf ( "Error retrieving debug information: %v" , err )
}
for key , value := range debug {
fname := fmt . Sprintf ( "%s/%s_%s.txt" , dirName , key , postfix )
handler , err = os . Create ( fname )
if err != nil {
return fmt . Errorf ( "Error creating file '%s': %v" , fname , err )
}
_ , err = handler . WriteString ( value )
if err != nil {
return fmt . Errorf ( "Error writing %s: %v" , key , err )
}
err = handler . Close ( )
if err != nil {
return fmt . Errorf ( "Error closing '%s': %v" , fname , err )
}
}
return nil
}