<commit_before>// Copyright 2016 Google Inc.
//
// 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 cli implements the commandline interface for Pixiecore.
package cli // import "go.universe.tf/netboot/pixiecore/cli"

import (
	"fmt"
	"io/ioutil"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"go.universe.tf/netboot/pixiecore"
)

// Ipxe is the set of ipxe binaries for supported firmwares.
//
// Can be set externally before calling CLI(), and set/extended by
// commandline processing in CLI().
var Ipxe = map[pixiecore.Firmware][]byte{}

// CLI runs the Pixiecore commandline.
//
// This function always exits back to the OS when finished.
func CLI() {
	if v1compatCLI() {
		return
	}

	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(-1)
	}
	os.Exit(0)
}

// This represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "pixiecore",
	Short: "All-in-one network booting",
	Long:  `Pixiecore is a tool to make network booting easy.`,
}

func initConfig() {
	viper.SetEnvPrefix("pixiecore")
	viper.AutomaticEnv() // read in environment variables that match
}

func fatalf(msg string, args ...interface{}) {
	fmt.Printf(msg+"\n", args...)
	os.Exit(1)
}

func todo(msg string, args ...interface{}) {
	fatalf("TODO: "+msg, args...)
}

func serverConfigFlags(cmd *cobra.Command) {
	cmd.Flags().BoolP("debug", "d", false, "Log more things that aren't directly related to booting a recognized client")
	cmd.Flags().BoolP("log-timestamps", "t", false, "Add a timestamp to each log line")
	cmd.Flags().IPP("listen-addr", "l", nil, "IPv4 address to listen on")
	cmd.Flags().IntP("port", "p", 80, "Port to listen on for HTTP")
	cmd.Flags().String("ipxe-bios", "", "path to an iPXE binary for BIOS/UNDI")
	cmd.Flags().String("ipxe-efi32", "", "path to an iPXE binary for 32-bit UEFI")
	cmd.Flags().String("ipxe-efi64", "", "path to an iPXE binary for 64-bit UEFI")
}

func mustFile(path string) []byte {
	bs, err := ioutil.ReadFile(path)
	if err != nil {
		fatalf("couldn't read file %q: %s", path, err)
	}

	return bs
}

func serverFromFlags(cmd *cobra.Command) *pixiecore.Server {
	debug, err := cmd.Flags().GetBool("debug")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	timestamps, err := cmd.Flags().GetBool("log-timestamps")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	addr, err := cmd.Flags().GetIP("listen-addr")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	httpPort, err := cmd.Flags().GetInt("port")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	ipxeBios, err := cmd.Flags().GetString("ipxe-bios")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	ipxeEFI32, err := cmd.Flags().GetString("ipxe-efi32")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	ipxeEFI64, err := cmd.Flags().GetString("ipxe-efi64")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}

	if addr != nil && addr.To4() == nil {
		fatalf("Listen address must be IPv4")
	}
	if httpPort <= 0 {
		fatalf("HTTP port must be >0")
	}

	ret := &pixiecore.Server{
		Ipxe:     map[pixiecore.Firmware][]byte{},
		Log:      logWithStdFmt,
		HTTPPort: httpPort,
	}
	for fwtype, bs := range Ipxe {
		ret.Ipxe[fwtype] = bs
	}
	if ipxeBios != "" {
		ret.Ipxe[pixiecore.FirmwareX86PC] = mustFile(ipxeBios)
	}
	if ipxeEFI32 != "" {
		ret.Ipxe[pixiecore.FirmwareEFI32] = mustFile(ipxeEFI32)
	}
	if ipxeEFI64 != "" {
		ret.Ipxe[pixiecore.FirmwareEFI64] = mustFile(ipxeEFI64)
	}

	if timestamps {
		ret.Log = logWithStdLog
	}
	if debug {
		ret.Debug = ret.Log
	}
	if addr != nil {
		ret.Address = addr.String()
	}

	return ret
}
<commit_msg>pixiecore/cli: default to a valid IPv4 listen address.<commit_after>// Copyright 2016 Google Inc.
//
// 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 cli implements the commandline interface for Pixiecore.
package cli // import "go.universe.tf/netboot/pixiecore/cli"

import (
	"fmt"
	"io/ioutil"
	"net"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"go.universe.tf/netboot/pixiecore"
)

// Ipxe is the set of ipxe binaries for supported firmwares.
//
// Can be set externally before calling CLI(), and set/extended by
// commandline processing in CLI().
var Ipxe = map[pixiecore.Firmware][]byte{}

// CLI runs the Pixiecore commandline.
//
// This function always exits back to the OS when finished.
func CLI() {
	if v1compatCLI() {
		return
	}

	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(-1)
	}
	os.Exit(0)
}

// This represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "pixiecore",
	Short: "All-in-one network booting",
	Long:  `Pixiecore is a tool to make network booting easy.`,
}

func initConfig() {
	viper.SetEnvPrefix("pixiecore")
	viper.AutomaticEnv() // read in environment variables that match
}

func fatalf(msg string, args ...interface{}) {
	fmt.Printf(msg+"\n", args...)
	os.Exit(1)
}

func todo(msg string, args ...interface{}) {
	fatalf("TODO: "+msg, args...)
}

func serverConfigFlags(cmd *cobra.Command) {
	cmd.Flags().BoolP("debug", "d", false, "Log more things that aren't directly related to booting a recognized client")
	cmd.Flags().BoolP("log-timestamps", "t", false, "Add a timestamp to each log line")
	cmd.Flags().IPP("listen-addr", "l", net.IPv4zero, "IPv4 address to listen on")
	cmd.Flags().IntP("port", "p", 80, "Port to listen on for HTTP")
	cmd.Flags().String("ipxe-bios", "", "path to an iPXE binary for BIOS/UNDI")
	cmd.Flags().String("ipxe-efi32", "", "path to an iPXE binary for 32-bit UEFI")
	cmd.Flags().String("ipxe-efi64", "", "path to an iPXE binary for 64-bit UEFI")
}

func mustFile(path string) []byte {
	bs, err := ioutil.ReadFile(path)
	if err != nil {
		fatalf("couldn't read file %q: %s", path, err)
	}

	return bs
}

func serverFromFlags(cmd *cobra.Command) *pixiecore.Server {
	debug, err := cmd.Flags().GetBool("debug")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	timestamps, err := cmd.Flags().GetBool("log-timestamps")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	addr, err := cmd.Flags().GetIP("listen-addr")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	httpPort, err := cmd.Flags().GetInt("port")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	ipxeBios, err := cmd.Flags().GetString("ipxe-bios")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	ipxeEFI32, err := cmd.Flags().GetString("ipxe-efi32")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}
	ipxeEFI64, err := cmd.Flags().GetString("ipxe-efi64")
	if err != nil {
		fatalf("Error reading flag: %s", err)
	}

	if addr != nil && addr.To4() == nil {
		fatalf("Listen address must be IPv4")
	}
	if httpPort <= 0 {
		fatalf("HTTP port must be >0")
	}

	ret := &pixiecore.Server{
		Ipxe:     map[pixiecore.Firmware][]byte{},
		Log:      logWithStdFmt,
		HTTPPort: httpPort,
	}
	for fwtype, bs := range Ipxe {
		ret.Ipxe[fwtype] = bs
	}
	if ipxeBios != "" {
		ret.Ipxe[pixiecore.FirmwareX86PC] = mustFile(ipxeBios)
	}
	if ipxeEFI32 != "" {
		ret.Ipxe[pixiecore.FirmwareEFI32] = mustFile(ipxeEFI32)
	}
	if ipxeEFI64 != "" {
		ret.Ipxe[pixiecore.FirmwareEFI64] = mustFile(ipxeEFI64)
	}

	if timestamps {
		ret.Log = logWithStdLog
	}
	if debug {
		ret.Debug = ret.Log
	}
	if addr != nil {
		ret.Address = addr.String()
	}

	return ret
}
<|endoftext|>


<commit_before>/*
Copyright 2015 The Kubernetes Authors.

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 api_test

import (
	"math/rand"
	"reflect"
	"testing"

	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/testapi"
	apitesting "k8s.io/kubernetes/pkg/api/testing"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/apimachinery/registered"
	"k8s.io/kubernetes/pkg/util/diff"

	"github.com/google/gofuzz"
)

func TestDeepCopyApiObjects(t *testing.T) {
	for i := 0; i < *fuzzIters; i++ {
		for _, version := range []unversioned.GroupVersion{testapi.Default.InternalGroupVersion(), registered.GroupOrDie(api.GroupName).GroupVersion} {
			f := apitesting.FuzzerFor(t, version, rand.NewSource(rand.Int63()))
			for kind := range api.Scheme.KnownTypes(version) {
				doDeepCopyTest(t, version.WithKind(kind), f)
			}
		}
	}
}

func doDeepCopyTest(t *testing.T, kind unversioned.GroupVersionKind, f *fuzz.Fuzzer) {
	item, err := api.Scheme.New(kind)
	if err != nil {
		t.Fatalf("Could not create a %v: %s", kind, err)
	}
	f.Fuzz(item)
	itemCopy, err := api.Scheme.DeepCopy(item)
	if err != nil {
		t.Errorf("Could not deep copy a %v: %s", kind, err)
		return
	}

	if !reflect.DeepEqual(item, itemCopy) {
		t.Errorf("\nexpected: %#v\n\ngot:      %#v\n\ndiff:      %v", item, itemCopy, diff.ObjectReflectDiff(item, itemCopy))
	}
}

func TestDeepCopySingleType(t *testing.T) {
	for i := 0; i < *fuzzIters; i++ {
		for _, version := range []unversioned.GroupVersion{testapi.Default.InternalGroupVersion(), registered.GroupOrDie(api.GroupName).GroupVersion} {
			f := apitesting.FuzzerFor(t, version, rand.NewSource(rand.Int63()))
			doDeepCopyTest(t, version.WithKind("Pod"), f)
		}
	}
}
<commit_msg>Add tests for deepcopy of structs<commit_after>/*
Copyright 2015 The Kubernetes Authors.

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 api_test

import (
	"bytes"
	"math/rand"
	"reflect"
	"testing"

	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/testapi"
	apitesting "k8s.io/kubernetes/pkg/api/testing"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/apimachinery/registered"
	"k8s.io/kubernetes/pkg/util/diff"

	"github.com/google/gofuzz"
)

func TestDeepCopyApiObjects(t *testing.T) {
	for i := 0; i < *fuzzIters; i++ {
		for _, version := range []unversioned.GroupVersion{testapi.Default.InternalGroupVersion(), registered.GroupOrDie(api.GroupName).GroupVersion} {
			f := apitesting.FuzzerFor(t, version, rand.NewSource(rand.Int63()))
			for kind := range api.Scheme.KnownTypes(version) {
				doDeepCopyTest(t, version.WithKind(kind), f)
			}
		}
	}
}

func doDeepCopyTest(t *testing.T, kind unversioned.GroupVersionKind, f *fuzz.Fuzzer) {
	item, err := api.Scheme.New(kind)
	if err != nil {
		t.Fatalf("Could not create a %v: %s", kind, err)
	}
	f.Fuzz(item)
	itemCopy, err := api.Scheme.DeepCopy(item)
	if err != nil {
		t.Errorf("Could not deep copy a %v: %s", kind, err)
		return
	}

	if !reflect.DeepEqual(item, itemCopy) {
		t.Errorf("\nexpected: %#v\n\ngot:      %#v\n\ndiff:      %v", item, itemCopy, diff.ObjectReflectDiff(item, itemCopy))
	}

	prefuzzData := &bytes.Buffer{}
	if err := api.Codecs.LegacyCodec(kind.GroupVersion()).Encode(item, prefuzzData); err != nil {
		t.Errorf("Could not encode a %v: %s", kind, err)
		return
	}

	// Refuzz the copy, which should have no effect on the original
	f.Fuzz(itemCopy)

	postfuzzData := &bytes.Buffer{}
	if err := api.Codecs.LegacyCodec(kind.GroupVersion()).Encode(item, postfuzzData); err != nil {
		t.Errorf("Could not encode a %v: %s", kind, err)
		return
	}

	if bytes.Compare(prefuzzData.Bytes(), postfuzzData.Bytes()) != 0 {
		t.Log(diff.StringDiff(prefuzzData.String(), postfuzzData.String()))
		t.Errorf("Fuzzing copy modified original of %#v", kind)
		return
	}
}

func TestDeepCopySingleType(t *testing.T) {
	for i := 0; i < *fuzzIters; i++ {
		for _, version := range []unversioned.GroupVersion{testapi.Default.InternalGroupVersion(), registered.GroupOrDie(api.GroupName).GroupVersion} {
			f := apitesting.FuzzerFor(t, version, rand.NewSource(rand.Int63()))
			doDeepCopyTest(t, version.WithKind("Pod"), f)
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2018 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package build

import (
	"fmt"
	"path/filepath"
	"time"

	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/sys/targets"
)

type fuchsia struct{}

func (fu fuchsia) build(targetArch, vmType, kernelDir, outputDir, compiler, userspaceDir,
	cmdlineFile, sysctlFile string, config []byte) error {
	sysTarget := targets.Get("fuchsia", targetArch)
	if sysTarget == nil {
		return fmt.Errorf("unsupported fuchsia arch %v", targetArch)
	}
	arch := sysTarget.KernelHeaderArch
	product := fmt.Sprintf("%s.%s", "core", arch)
	if _, err := osutil.RunCmd(time.Hour, kernelDir, "scripts/fx", "set", product,
		"--args", `extra_authorized_keys_file="//.ssh/authorized_keys"`,
		"--with-base", "//bundles:tools",
		"--build-dir", "out/"+arch); err != nil {
		return err
	}
	if _, err := osutil.RunCmd(time.Hour, kernelDir, "scripts/fx", "clean-build"); err != nil {
		return err
	}
	for src, dst := range map[string]string{
		"out/" + arch + "/obj/build/images/fvm.blk": "image",
		".ssh/pkey": "key",
		"out/" + arch + ".zircon/kernel-" + arch + "-gcc/obj/kernel/zircon.elf": "obj/zircon.elf",
		"out/" + arch + ".zircon/multiboot.bin":                                 "kernel",
		"out/" + arch + "/fuchsia.zbi":                                          "initrd",
	} {
		fullSrc := filepath.Join(kernelDir, filepath.FromSlash(src))
		fullDst := filepath.Join(outputDir, filepath.FromSlash(dst))
		if err := osutil.CopyFile(fullSrc, fullDst); err != nil {
			return fmt.Errorf("failed to copy %v: %v", src, err)
		}
	}
	return nil
}

func (fu fuchsia) clean(kernelDir, targetArch string) error {
	// We always do clean build because incremental build is frequently broken.
	// So no need to clean separately.
	return nil
}
<commit_msg>pkg/build: Add ssh keys for fuchsia<commit_after>// Copyright 2018 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package build

import (
	"fmt"
	"path/filepath"
	"time"

	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/sys/targets"
)

type fuchsia struct{}

func (fu fuchsia) build(targetArch, vmType, kernelDir, outputDir, compiler, userspaceDir,
	cmdlineFile, sysctlFile string, config []byte) error {
	sysTarget := targets.Get("fuchsia", targetArch)
	if sysTarget == nil {
		return fmt.Errorf("unsupported fuchsia arch %v", targetArch)
	}
	arch := sysTarget.KernelHeaderArch
	product := fmt.Sprintf("%s.%s", "core", arch)
	if _, err := osutil.RunCmd(time.Hour, kernelDir, "scripts/fx", "--dir", "out/"+arch,
		"set", product, "--with-base", "//bundles:tools"); err != nil {
		return err
	}
	if _, err := osutil.RunCmd(time.Hour, kernelDir, "scripts/fx", "clean-build"); err != nil {
		return err
	}

	// Fuchsia images no longer include ssh keys. Manually append the ssh public key to the zbi.
	sshZBI := filepath.Join(kernelDir, "out", arch, "fuchsia-ssh.zbi")
	kernelZBI := filepath.Join(kernelDir, "out", arch, "fuchsia.zbi")
	authorizedKeys := fmt.Sprintf("data/ssh/authorized_keys=%s", filepath.Join(kernelDir, ".ssh", "authorized_keys"))
	if _, err := osutil.RunCmd(time.Minute, kernelDir, "out/"+arch+".zircon/tools/zbi",
		"-o", sshZBI, kernelZBI, "--entry", authorizedKeys); err != nil {
		return err
	}

	for src, dst := range map[string]string{
		"out/" + arch + "/obj/build/images/fvm.blk": "image",
		".ssh/pkey": "key",
		"out/" + arch + ".zircon/kernel-" + arch + "-gcc/obj/kernel/zircon.elf": "obj/zircon.elf",
		"out/" + arch + ".zircon/multiboot.bin":                                 "kernel",
		"out/" + arch + "/fuchsia-ssh.zbi":                                      "initrd",
	} {
		fullSrc := filepath.Join(kernelDir, filepath.FromSlash(src))
		fullDst := filepath.Join(outputDir, filepath.FromSlash(dst))
		if err := osutil.CopyFile(fullSrc, fullDst); err != nil {
			return fmt.Errorf("failed to copy %v: %v", src, err)
		}
	}
	return nil
}

func (fu fuchsia) clean(kernelDir, targetArch string) error {
	// We always do clean build because incremental build is frequently broken.
	// So no need to clean separately.
	return nil
}
<|endoftext|>


<commit_before>package client

import (
	"github.com/golang/glog"
	minio "github.com/minio/minio-go"
)

// MinioClient is a wrapper around minio.Client that also holds the bucket name
// where we want to copy files.
type MinioClient struct {
	Client          *minio.Client
	BucketName      string
	ServerURI       string
	AccesKeyID      string
	SecretAccessKey string
}

// NewMinioClient returns a new minio client based on passed access specs and
// creates a new bucket if it doesn't exist.
func NewMinioClient(serverURI, accessKeyID, secretAccessKey, bucket string, secure bool) (*MinioClient, error) {
	c, err := minio.New(serverURI, accessKeyID, secretAccessKey, secure)

	glog.V(0).Infof("%s - %s - %s - %s - %t - %#v", serverURI, accessKeyID, secretAccessKey, bucket, secure, c)
	if err != nil {
		return nil, err
	}

	return &MinioClient{
		Client:          c,
		ServerURI:       serverURI,
		AccesKeyID:      accessKeyID,
		SecretAccessKey: secretAccessKey,
		BucketName:      bucket,
	}, nil
}
<commit_msg>remove debugging<commit_after>package client

import (
	minio "github.com/minio/minio-go"
)

// MinioClient is a wrapper around minio.Client that also holds the bucket name
// where we want to copy files.
type MinioClient struct {
	Client          *minio.Client
	BucketName      string
	ServerURI       string
	AccesKeyID      string
	SecretAccessKey string
}

// NewMinioClient returns a new minio client based on passed access specs and
// creates a new bucket if it doesn't exist.
func NewMinioClient(serverURI, accessKeyID, secretAccessKey, bucket string, secure bool) (*MinioClient, error) {
	c, err := minio.New(serverURI, accessKeyID, secretAccessKey, secure)

	if err != nil {
		return nil, err
	}

	return &MinioClient{
		Client:          c,
		ServerURI:       serverURI,
		AccesKeyID:      accessKeyID,
		SecretAccessKey: secretAccessKey,
		BucketName:      bucket,
	}, nil
}
<|endoftext|>


<commit_before>/*
Copyright 2014 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package client

import (
	"fmt"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"reflect"
	gruntime "runtime"
	"strings"
	"time"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
)

// Config holds the common attributes that can be passed to a Kubernetes client on
// initialization.
type Config struct {
	// Host must be a host string, a host:port pair, or a URL to the base of the API.
	Host string
	// Prefix is the sub path of the server. If not specified, the client will set
	// a default value.  Use "/" to indicate the server root should be used
	Prefix string
	// Version is the API version to talk to. Must be provided when initializing
	// a RESTClient directly. When initializing a Client, will be set with the default
	// code version.
	Version string
	// LegacyBehavior defines whether the RESTClient should follow conventions that
	// existed prior to v1beta3 in Kubernetes - namely, namespace (if specified)
	// not being part of the path, and resource names allowing mixed case. Set to
	// true when using Kubernetes v1beta1 or v1beta2.
	LegacyBehavior bool
	// Codec specifies the encoding and decoding behavior for runtime.Objects passed
	// to a RESTClient or Client. Required when initializing a RESTClient, optional
	// when initializing a Client.
	Codec runtime.Codec

	// Server requires Basic authentication
	Username string
	Password string

	// Server requires Bearer authentication. This client will not attempt to use
	// refresh tokens for an OAuth2 flow.
	// TODO: demonstrate an OAuth2 compatible client.
	BearerToken string

	// TLSClientConfig contains settings to enable transport layer security
	TLSClientConfig

	// Server should be accessed without verifying the TLS
	// certificate. For testing only.
	Insecure bool

	// UserAgent is an optional field that specifies the caller of this request.
	UserAgent string

	// Transport may be used for custom HTTP behavior. This attribute may not
	// be specified with the TLS client certificate options.
	Transport http.RoundTripper

	// QPS indicates the maximum QPS to the master from this client.  If zero, QPS is unlimited.
	QPS float32

	// Maximum burst for throttle
	Burst int
}

type KubeletConfig struct {
	// ToDo: Add support for different kubelet instances exposing different ports
	Port        uint
	EnableHttps bool

	// TLSClientConfig contains settings to enable transport layer security
	TLSClientConfig

	// HTTPTimeout is used by the client to timeout http requests to Kubelet.
	HTTPTimeout time.Duration
}

// TLSClientConfig contains settings to enable transport layer security
type TLSClientConfig struct {
	// Server requires TLS client certificate authentication
	CertFile string
	// Server requires TLS client certificate authentication
	KeyFile string
	// Trusted root certificates for server
	CAFile string

	// CertData holds PEM-encoded bytes (typically read from a client certificate file).
	// CertData takes precedence over CertFile
	CertData []byte
	// KeyData holds PEM-encoded bytes (typically read from a client certificate key file).
	// KeyData takes precedence over KeyFile
	KeyData []byte
	// CAData holds PEM-encoded bytes (typically read from a root certificates bundle).
	// CAData takes precedence over CAFile
	CAData []byte
}

// New creates a Kubernetes client for the given config. This client works with pods,
// replication controllers and services. It allows operations such as list, get, update
// and delete on these objects. An error is returned if the provided configuration
// is not valid.
func New(c *Config) (*Client, error) {
	config := *c
	if err := SetKubernetesDefaults(&config); err != nil {
		return nil, err
	}
	client, err := RESTClientFor(&config)
	if err != nil {
		return nil, err
	}
	return &Client{client}, nil
}

func MatchesServerVersion(c *Config) error {
	client, err := New(c)
	if err != nil {
		return err
	}

	clientVersion := version.Get()
	serverVersion, err := client.ServerVersion()
	if err != nil {
		return fmt.Errorf("couldn't read version from server: %v\n", err)
	}
	if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) {
		return fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion)
	}

	return nil
}

// NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized.
func NewOrDie(c *Config) *Client {
	client, err := New(c)
	if err != nil {
		panic(err)
	}
	return client
}

// SetKubernetesDefaults sets default values on the provided client config for accessing the
// Kubernetes API or returns an error if any of the defaults are impossible or invalid.
func SetKubernetesDefaults(config *Config) error {
	if config.Prefix == "" {
		config.Prefix = "/api"
	}
	if len(config.UserAgent) == 0 {
		config.UserAgent = DefaultKubernetesUserAgent()
	}
	if len(config.Version) == 0 {
		config.Version = defaultVersionFor(config)
	}
	version := config.Version
	versionInterfaces, err := latest.InterfacesFor(version)
	if err != nil {
		return fmt.Errorf("API version '%s' is not recognized (valid values: %s)", version, strings.Join(latest.Versions, ", "))
	}
	if config.Codec == nil {
		config.Codec = versionInterfaces.Codec
	}
	config.LegacyBehavior = (version == "v1beta1" || version == "v1beta2")
	if config.QPS == 0.0 {
		config.QPS = 5.0
	}
	if config.Burst == 0 {
		config.Burst = 10
	}
	return nil
}

// RESTClientFor returns a RESTClient that satisfies the requested attributes on a client Config
// object. Note that a RESTClient may require fields that are optional when initializing a Client.
// A RESTClient created by this method is generic - it expects to operate on an API that follows
// the Kubernetes conventions, but may not be the Kubernetes API.
func RESTClientFor(config *Config) (*RESTClient, error) {
	if len(config.Version) == 0 {
		return nil, fmt.Errorf("version is required when initializing a RESTClient")
	}
	if config.Codec == nil {
		return nil, fmt.Errorf("Codec is required when initializing a RESTClient")
	}

	baseURL, err := defaultServerUrlFor(config)
	if err != nil {
		return nil, err
	}

	client := NewRESTClient(baseURL, config.Version, config.Codec, config.LegacyBehavior, config.QPS, config.Burst)

	transport, err := TransportFor(config)
	if err != nil {
		return nil, err
	}

	if transport != http.DefaultTransport {
		client.Client = &http.Client{Transport: transport}
	}
	return client, nil
}

// TransportFor returns an http.RoundTripper that will provide the authentication
// or transport level security defined by the provided Config. Will return the
// default http.DefaultTransport if no special case behavior is needed.
func TransportFor(config *Config) (http.RoundTripper, error) {
	hasCA := len(config.CAFile) > 0 || len(config.CAData) > 0
	hasCert := len(config.CertFile) > 0 || len(config.CertData) > 0

	// Set transport level security
	if config.Transport != nil && (hasCA || hasCert || config.Insecure) {
		return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed")
	}

	tlsConfig, err := TLSConfigFor(config)
	if err != nil {
		return nil, err
	}

	var transport http.RoundTripper
	if config.Transport != nil {
		transport = config.Transport
	} else {
		if tlsConfig != nil {
			transport = &http.Transport{
				TLSClientConfig: tlsConfig,
				Proxy:           http.ProxyFromEnvironment,
				Dial: (&net.Dialer{
					Timeout:   30 * time.Second,
					KeepAlive: 30 * time.Second,
				}).Dial,
				TLSHandshakeTimeout: 10 * time.Second,
			}
		} else {
			transport = http.DefaultTransport
		}
	}

	transport, err = HTTPWrappersForConfig(config, transport)
	if err != nil {
		return nil, err
	}

	// TODO: use the config context to wrap a transport

	return transport, nil
}

// HTTPWrappersForConfig wraps a round tripper with any relevant layered behavior from the
// config. Exposed to allow more clients that need HTTP-like behavior but then must hijack
// the underlying connection (like WebSocket or HTTP2 clients). Pure HTTP clients should use
// the higher level TransportFor or RESTClientFor methods.
func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) {
	// Set authentication wrappers
	hasBasicAuth := config.Username != "" || config.Password != ""
	if hasBasicAuth && config.BearerToken != "" {
		return nil, fmt.Errorf("username/password or bearer token may be set, but not both")
	}
	switch {
	case config.BearerToken != "":
		rt = NewBearerAuthRoundTripper(config.BearerToken, rt)
	case hasBasicAuth:
		rt = NewBasicAuthRoundTripper(config.Username, config.Password, rt)
	}
	if len(config.UserAgent) > 0 {
		rt = NewUserAgentRoundTripper(config.UserAgent, rt)
	}
	return rt, nil
}

// DefaultServerURL converts a host, host:port, or URL string to the default base server API path
// to use with a Client at a given API version following the standard conventions for a
// Kubernetes API.
func DefaultServerURL(host, prefix, version string, defaultTLS bool) (*url.URL, error) {
	if host == "" {
		return nil, fmt.Errorf("host must be a URL or a host:port pair")
	}
	if version == "" {
		return nil, fmt.Errorf("version must be set")
	}
	base := host
	hostURL, err := url.Parse(base)
	if err != nil {
		return nil, err
	}
	if hostURL.Scheme == "" {
		scheme := "http://"
		if defaultTLS {
			scheme = "https://"
		}
		hostURL, err = url.Parse(scheme + base)
		if err != nil {
			return nil, err
		}
		if hostURL.Path != "" && hostURL.Path != "/" {
			return nil, fmt.Errorf("host must be a URL or a host:port pair: %s", base)
		}
	}

	// If the user specified a URL without a path component (http://server.com), automatically
	// append the default prefix
	if hostURL.Path == "" {
		if prefix == "" {
			prefix = "/"
		}
		hostURL.Path = prefix
	}

	// Add the version to the end of the path
	hostURL.Path = path.Join(hostURL.Path, version)

	return hostURL, nil
}

// IsConfigTransportTLS returns true iff the provided config will result in a protected
// connection to the server when it is passed to client.New() or client.RESTClientFor().
// Use to determine when to send credentials over the wire.
//
// Note: the Insecure flag is ignored when testing for this value, so MITM attacks are
// still possible.
func IsConfigTransportTLS(config Config) bool {
	// determination of TLS transport does not logically require a version to be specified
	// modify the copy of the config we got to satisfy preconditions for defaultServerUrlFor
	config.Version = defaultVersionFor(&config)

	baseURL, err := defaultServerUrlFor(&config)
	if err != nil {
		return false
	}
	return baseURL.Scheme == "https"
}

// defaultServerUrlFor is shared between IsConfigTransportTLS and RESTClientFor. It
// requires Host and Version to be set prior to being called.
func defaultServerUrlFor(config *Config) (*url.URL, error) {
	// TODO: move the default to secure when the apiserver supports TLS by default
	// config.Insecure is taken to mean "I want HTTPS but don't bother checking the certs against a CA."
	hasCA := len(config.CAFile) != 0 || len(config.CAData) != 0
	hasCert := len(config.CertFile) != 0 || len(config.CertData) != 0
	defaultTLS := hasCA || hasCert || config.Insecure
	host := config.Host
	if host == "" {
		host = "localhost"
	}
	return DefaultServerURL(host, config.Prefix, config.Version, defaultTLS)
}

// defaultVersionFor is shared between defaultServerUrlFor and RESTClientFor
func defaultVersionFor(config *Config) string {
	version := config.Version
	if version == "" {
		// Clients default to the preferred code API version
		// TODO: implement version negotiation (highest version supported by server)
		version = latest.Version
	}
	return version
}

// DefaultKubernetesUserAgent returns the default user agent that clients can use.
func DefaultKubernetesUserAgent() string {
	commit := version.Get().GitCommit
	if len(commit) > 7 {
		commit = commit[:7]
	}
	if len(commit) == 0 {
		commit = "unknown"
	}
	version := version.Get().GitVersion
	seg := strings.SplitN(version, "-", 2)
	version = seg[0]
	return fmt.Sprintf("%s/%s (%s/%s) kubernetes/%s", path.Base(os.Args[0]), version, gruntime.GOOS, gruntime.GOARCH, commit)
}
<commit_msg>Allow client.Config to wrap the underyling Transport<commit_after>/*
Copyright 2014 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package client

import (
	"fmt"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"reflect"
	gruntime "runtime"
	"strings"
	"time"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
)

// Config holds the common attributes that can be passed to a Kubernetes client on
// initialization.
type Config struct {
	// Host must be a host string, a host:port pair, or a URL to the base of the API.
	Host string
	// Prefix is the sub path of the server. If not specified, the client will set
	// a default value.  Use "/" to indicate the server root should be used
	Prefix string
	// Version is the API version to talk to. Must be provided when initializing
	// a RESTClient directly. When initializing a Client, will be set with the default
	// code version.
	Version string
	// LegacyBehavior defines whether the RESTClient should follow conventions that
	// existed prior to v1beta3 in Kubernetes - namely, namespace (if specified)
	// not being part of the path, and resource names allowing mixed case. Set to
	// true when using Kubernetes v1beta1 or v1beta2.
	LegacyBehavior bool
	// Codec specifies the encoding and decoding behavior for runtime.Objects passed
	// to a RESTClient or Client. Required when initializing a RESTClient, optional
	// when initializing a Client.
	Codec runtime.Codec

	// Server requires Basic authentication
	Username string
	Password string

	// Server requires Bearer authentication. This client will not attempt to use
	// refresh tokens for an OAuth2 flow.
	// TODO: demonstrate an OAuth2 compatible client.
	BearerToken string

	// TLSClientConfig contains settings to enable transport layer security
	TLSClientConfig

	// Server should be accessed without verifying the TLS
	// certificate. For testing only.
	Insecure bool

	// UserAgent is an optional field that specifies the caller of this request.
	UserAgent string

	// Transport may be used for custom HTTP behavior. This attribute may not
	// be specified with the TLS client certificate options. Use WrapTransport
	// for most client level operations.
	Transport http.RoundTripper
	// WrapTransport will be invoked for custom HTTP behavior after the underlying
	// transport is initialized (either the transport created from TLSClientConfig,
	// Transport, or http.DefaultTransport). The config may layer other RoundTrippers
	// on top of the returned RoundTripper.
	WrapTransport func(rt http.RoundTripper) http.RoundTripper

	// QPS indicates the maximum QPS to the master from this client.  If zero, QPS is unlimited.
	QPS float32

	// Maximum burst for throttle
	Burst int
}

type KubeletConfig struct {
	// ToDo: Add support for different kubelet instances exposing different ports
	Port        uint
	EnableHttps bool

	// TLSClientConfig contains settings to enable transport layer security
	TLSClientConfig

	// HTTPTimeout is used by the client to timeout http requests to Kubelet.
	HTTPTimeout time.Duration
}

// TLSClientConfig contains settings to enable transport layer security
type TLSClientConfig struct {
	// Server requires TLS client certificate authentication
	CertFile string
	// Server requires TLS client certificate authentication
	KeyFile string
	// Trusted root certificates for server
	CAFile string

	// CertData holds PEM-encoded bytes (typically read from a client certificate file).
	// CertData takes precedence over CertFile
	CertData []byte
	// KeyData holds PEM-encoded bytes (typically read from a client certificate key file).
	// KeyData takes precedence over KeyFile
	KeyData []byte
	// CAData holds PEM-encoded bytes (typically read from a root certificates bundle).
	// CAData takes precedence over CAFile
	CAData []byte
}

// New creates a Kubernetes client for the given config. This client works with pods,
// replication controllers and services. It allows operations such as list, get, update
// and delete on these objects. An error is returned if the provided configuration
// is not valid.
func New(c *Config) (*Client, error) {
	config := *c
	if err := SetKubernetesDefaults(&config); err != nil {
		return nil, err
	}
	client, err := RESTClientFor(&config)
	if err != nil {
		return nil, err
	}
	return &Client{client}, nil
}

func MatchesServerVersion(c *Config) error {
	client, err := New(c)
	if err != nil {
		return err
	}

	clientVersion := version.Get()
	serverVersion, err := client.ServerVersion()
	if err != nil {
		return fmt.Errorf("couldn't read version from server: %v\n", err)
	}
	if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) {
		return fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion)
	}

	return nil
}

// NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized.
func NewOrDie(c *Config) *Client {
	client, err := New(c)
	if err != nil {
		panic(err)
	}
	return client
}

// SetKubernetesDefaults sets default values on the provided client config for accessing the
// Kubernetes API or returns an error if any of the defaults are impossible or invalid.
func SetKubernetesDefaults(config *Config) error {
	if config.Prefix == "" {
		config.Prefix = "/api"
	}
	if len(config.UserAgent) == 0 {
		config.UserAgent = DefaultKubernetesUserAgent()
	}
	if len(config.Version) == 0 {
		config.Version = defaultVersionFor(config)
	}
	version := config.Version
	versionInterfaces, err := latest.InterfacesFor(version)
	if err != nil {
		return fmt.Errorf("API version '%s' is not recognized (valid values: %s)", version, strings.Join(latest.Versions, ", "))
	}
	if config.Codec == nil {
		config.Codec = versionInterfaces.Codec
	}
	config.LegacyBehavior = (version == "v1beta1" || version == "v1beta2")
	if config.QPS == 0.0 {
		config.QPS = 5.0
	}
	if config.Burst == 0 {
		config.Burst = 10
	}
	return nil
}

// RESTClientFor returns a RESTClient that satisfies the requested attributes on a client Config
// object. Note that a RESTClient may require fields that are optional when initializing a Client.
// A RESTClient created by this method is generic - it expects to operate on an API that follows
// the Kubernetes conventions, but may not be the Kubernetes API.
func RESTClientFor(config *Config) (*RESTClient, error) {
	if len(config.Version) == 0 {
		return nil, fmt.Errorf("version is required when initializing a RESTClient")
	}
	if config.Codec == nil {
		return nil, fmt.Errorf("Codec is required when initializing a RESTClient")
	}

	baseURL, err := defaultServerUrlFor(config)
	if err != nil {
		return nil, err
	}

	client := NewRESTClient(baseURL, config.Version, config.Codec, config.LegacyBehavior, config.QPS, config.Burst)

	transport, err := TransportFor(config)
	if err != nil {
		return nil, err
	}

	if transport != http.DefaultTransport {
		client.Client = &http.Client{Transport: transport}
	}
	return client, nil
}

// TransportFor returns an http.RoundTripper that will provide the authentication
// or transport level security defined by the provided Config. Will return the
// default http.DefaultTransport if no special case behavior is needed.
func TransportFor(config *Config) (http.RoundTripper, error) {
	hasCA := len(config.CAFile) > 0 || len(config.CAData) > 0
	hasCert := len(config.CertFile) > 0 || len(config.CertData) > 0

	// Set transport level security
	if config.Transport != nil && (hasCA || hasCert || config.Insecure) {
		return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed")
	}

	tlsConfig, err := TLSConfigFor(config)
	if err != nil {
		return nil, err
	}

	var transport http.RoundTripper
	if config.Transport != nil {
		transport = config.Transport
	} else {
		if tlsConfig != nil {
			transport = &http.Transport{
				TLSClientConfig: tlsConfig,
				Proxy:           http.ProxyFromEnvironment,
				Dial: (&net.Dialer{
					Timeout:   30 * time.Second,
					KeepAlive: 30 * time.Second,
				}).Dial,
				TLSHandshakeTimeout: 10 * time.Second,
			}
		} else {
			transport = http.DefaultTransport
		}
	}
	if config.WrapTransport != nil {
		transport = config.WrapTransport(transport)
	}

	transport, err = HTTPWrappersForConfig(config, transport)
	if err != nil {
		return nil, err
	}

	// TODO: use the config context to wrap a transport

	return transport, nil
}

// HTTPWrappersForConfig wraps a round tripper with any relevant layered behavior from the
// config. Exposed to allow more clients that need HTTP-like behavior but then must hijack
// the underlying connection (like WebSocket or HTTP2 clients). Pure HTTP clients should use
// the higher level TransportFor or RESTClientFor methods.
func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) {
	// Set authentication wrappers
	hasBasicAuth := config.Username != "" || config.Password != ""
	if hasBasicAuth && config.BearerToken != "" {
		return nil, fmt.Errorf("username/password or bearer token may be set, but not both")
	}
	switch {
	case config.BearerToken != "":
		rt = NewBearerAuthRoundTripper(config.BearerToken, rt)
	case hasBasicAuth:
		rt = NewBasicAuthRoundTripper(config.Username, config.Password, rt)
	}
	if len(config.UserAgent) > 0 {
		rt = NewUserAgentRoundTripper(config.UserAgent, rt)
	}
	return rt, nil
}

// DefaultServerURL converts a host, host:port, or URL string to the default base server API path
// to use with a Client at a given API version following the standard conventions for a
// Kubernetes API.
func DefaultServerURL(host, prefix, version string, defaultTLS bool) (*url.URL, error) {
	if host == "" {
		return nil, fmt.Errorf("host must be a URL or a host:port pair")
	}
	if version == "" {
		return nil, fmt.Errorf("version must be set")
	}
	base := host
	hostURL, err := url.Parse(base)
	if err != nil {
		return nil, err
	}
	if hostURL.Scheme == "" {
		scheme := "http://"
		if defaultTLS {
			scheme = "https://"
		}
		hostURL, err = url.Parse(scheme + base)
		if err != nil {
			return nil, err
		}
		if hostURL.Path != "" && hostURL.Path != "/" {
			return nil, fmt.Errorf("host must be a URL or a host:port pair: %s", base)
		}
	}

	// If the user specified a URL without a path component (http://server.com), automatically
	// append the default prefix
	if hostURL.Path == "" {
		if prefix == "" {
			prefix = "/"
		}
		hostURL.Path = prefix
	}

	// Add the version to the end of the path
	hostURL.Path = path.Join(hostURL.Path, version)

	return hostURL, nil
}

// IsConfigTransportTLS returns true iff the provided config will result in a protected
// connection to the server when it is passed to client.New() or client.RESTClientFor().
// Use to determine when to send credentials over the wire.
//
// Note: the Insecure flag is ignored when testing for this value, so MITM attacks are
// still possible.
func IsConfigTransportTLS(config Config) bool {
	// determination of TLS transport does not logically require a version to be specified
	// modify the copy of the config we got to satisfy preconditions for defaultServerUrlFor
	config.Version = defaultVersionFor(&config)

	baseURL, err := defaultServerUrlFor(&config)
	if err != nil {
		return false
	}
	return baseURL.Scheme == "https"
}

// defaultServerUrlFor is shared between IsConfigTransportTLS and RESTClientFor. It
// requires Host and Version to be set prior to being called.
func defaultServerUrlFor(config *Config) (*url.URL, error) {
	// TODO: move the default to secure when the apiserver supports TLS by default
	// config.Insecure is taken to mean "I want HTTPS but don't bother checking the certs against a CA."
	hasCA := len(config.CAFile) != 0 || len(config.CAData) != 0
	hasCert := len(config.CertFile) != 0 || len(config.CertData) != 0
	defaultTLS := hasCA || hasCert || config.Insecure
	host := config.Host
	if host == "" {
		host = "localhost"
	}
	return DefaultServerURL(host, config.Prefix, config.Version, defaultTLS)
}

// defaultVersionFor is shared between defaultServerUrlFor and RESTClientFor
func defaultVersionFor(config *Config) string {
	version := config.Version
	if version == "" {
		// Clients default to the preferred code API version
		// TODO: implement version negotiation (highest version supported by server)
		version = latest.Version
	}
	return version
}

// DefaultKubernetesUserAgent returns the default user agent that clients can use.
func DefaultKubernetesUserAgent() string {
	commit := version.Get().GitCommit
	if len(commit) > 7 {
		commit = commit[:7]
	}
	if len(commit) == 0 {
		commit = "unknown"
	}
	version := version.Get().GitVersion
	seg := strings.SplitN(version, "-", 2)
	version = seg[0]
	return fmt.Sprintf("%s/%s (%s/%s) kubernetes/%s", path.Base(os.Args[0]), version, gruntime.GOOS, gruntime.GOARCH, commit)
}
<|endoftext|>


<commit_before>/*
Copyright 2011 Google Inc.

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 client

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"camlistore.org/pkg/blobref"
)

var _ = log.Printf

// multipartOverhead is how many extra bytes mime/multipart's
// Writer adds around content
var multipartOverhead = calculateMultipartOverhead()

type UploadHandle struct {
	BlobRef  *blobref.BlobRef
	Size     int64 // or -1 if size isn't known
	Contents io.Reader
	Vivify   bool
}

type PutResult struct {
	BlobRef *blobref.BlobRef
	Size    int64
	Skipped bool // already present on blobserver
}

func (pr *PutResult) SizedBlobRef() blobref.SizedBlobRef {
	return blobref.SizedBlobRef{pr.BlobRef, pr.Size}
}

type statResponse struct {
	HaveMap                    map[string]blobref.SizedBlobRef
	maxUploadSize              int64
	uploadUrl                  string
	uploadUrlExpirationSeconds int
	canLongPoll                bool
}

type ResponseFormatError error

func calculateMultipartOverhead() int64 {
	var b bytes.Buffer
	w := multipart.NewWriter(&b)
	part, _ := w.CreateFormFile("0", "0")

	dummyContents := []byte("0")
	part.Write(dummyContents)

	w.Close()
	return int64(b.Len()) - 3 // remove what was added
}

func newResFormatError(s string, arg ...interface{}) ResponseFormatError {
	return ResponseFormatError(fmt.Errorf(s, arg...))
}

// TODO-GO: if outerr is replaced by a "_", gotest(!) fails with a 6g error.
func parseStatResponse(r io.Reader) (sr *statResponse, outerr error) {
	var (
		ok   bool
		err  error
		s    = &statResponse{HaveMap: make(map[string]blobref.SizedBlobRef)}
		jmap = make(map[string]interface{})
	)
	if err = json.NewDecoder(io.LimitReader(r, 5<<20)).Decode(&jmap); err != nil {
		return nil, ResponseFormatError(err)
	}
	defer func() {
		if sr == nil {
			log.Printf("parseStatResponse got map: %#v", jmap)
		}
	}()

	s.uploadUrl, ok = jmap["uploadUrl"].(string)
	if !ok {
		return nil, newResFormatError("no 'uploadUrl' in stat response")
	}

	if n, ok := jmap["maxUploadSize"].(float64); ok {
		s.maxUploadSize = int64(n)
	} else {
		return nil, newResFormatError("no 'maxUploadSize' in stat response")
	}

	if n, ok := jmap["uploadUrlExpirationSeconds"].(float64); ok {
		s.uploadUrlExpirationSeconds = int(n)
	} else {
		return nil, newResFormatError("no 'uploadUrlExpirationSeconds' in stat response")
	}

	if v, ok := jmap["canLongPoll"].(bool); ok {
		s.canLongPoll = v
	}

	alreadyHave, ok := jmap["stat"].([]interface{})
	if !ok {
		return nil, newResFormatError("no 'stat' key in stat response")
	}

	for _, li := range alreadyHave {
		m, ok := li.(map[string]interface{})
		if !ok {
			return nil, newResFormatError("'stat' list value of unexpected type %T", li)
		}
		blobRefStr, ok := m["blobRef"].(string)
		if !ok {
			return nil, newResFormatError("'stat' list item has non-string 'blobRef' key")
		}
		size, ok := m["size"].(float64)
		if !ok {
			return nil, newResFormatError("'stat' list item has non-number 'size' key")
		}
		br := blobref.Parse(blobRefStr)
		if br == nil {
			return nil, newResFormatError("'stat' list item has invalid 'blobRef' key")
		}
		s.HaveMap[br.String()] = blobref.SizedBlobRef{br, int64(size)}
	}

	return s, nil
}

func NewUploadHandleFromString(data string) *UploadHandle {
	bref := blobref.SHA1FromString(data)
	r := strings.NewReader(data)
	return &UploadHandle{BlobRef: bref, Size: int64(len(data)), Contents: r}
}

func (c *Client) jsonFromResponse(requestName string, resp *http.Response) (map[string]interface{}, error) {
	if resp.StatusCode != 200 {
		log.Printf("After %s request, failed to JSON from response; status code is %d", requestName, resp.StatusCode)
		io.Copy(os.Stderr, resp.Body)
		return nil, errors.New(fmt.Sprintf("After %s request, HTTP response code is %d; no JSON to parse.", requestName, resp.StatusCode))
	}
	// TODO: LimitReader here for paranoia
	buf := new(bytes.Buffer)
	io.Copy(buf, resp.Body)
	resp.Body.Close()
	jmap := make(map[string]interface{})
	if jerr := json.Unmarshal(buf.Bytes(), &jmap); jerr != nil {
		return nil, jerr
	}
	return jmap, nil
}

func (c *Client) StatBlobs(dest chan<- blobref.SizedBlobRef, blobs []*blobref.BlobRef, wait time.Duration) error {
	if len(blobs) == 0 {
		return nil
	}

	// TODO: if len(blobs) > 1000 or something, cut this up into
	// multiple http requests, and also if the server returns a
	// 400 error, per the blob-stat-protocol.txt document.
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "camliversion=1")
	for n, blob := range blobs {
		if blob == nil {
			panic("nil blob")
		}
		fmt.Fprintf(&buf, "&blob%d=%s", n+1, blob)
	}

	if wait > 0 {
		secs := int(wait.Seconds())
		if secs == 0 {
			secs = 1
		}
		fmt.Fprintf(&buf, "&maxwaitsec=%d", secs)
	}

	pfx, err := c.prefix()
	if err != nil {
		return err
	}
	req := c.newRequest("POST", fmt.Sprintf("%s/camli/stat", pfx))
	bodyStr := buf.String()
	req.Body = ioutil.NopCloser(strings.NewReader(bodyStr))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.ContentLength = int64(len(bodyStr))

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("stat HTTP error: %v", err)
	}
	if resp.Body != nil {
		defer resp.Body.Close()
	}

	if resp.StatusCode != 200 {
		return fmt.Errorf("stat response had http status %d", resp.StatusCode)
	}

	stat, err := parseStatResponse(resp.Body)
	if err != nil {
		return err
	}

	for _, sb := range stat.HaveMap {
		dest <- sb
	}
	return nil
}

// Figure out the size of the contents.
// If the size was provided, trust it.
// If the size was not provided (-1), slurp.
func readerAndSize(h *UploadHandle) (io.Reader, int64, error) {
	if h.Size != -1 {
		return h.Contents, h.Size, nil
	}
	var b bytes.Buffer
	n, err := io.Copy(&b, h.Contents)
	if err != nil {
		return nil, 0, err
	}
	return &b, n, nil
}

func (c *Client) Upload(h *UploadHandle) (*PutResult, error) {
	errorf := func(msg string, arg ...interface{}) (*PutResult, error) {
		err := fmt.Errorf(msg, arg...)
		c.log.Print(err.Error())
		return nil, err
	}

	bodyReader, bodySize, err := readerAndSize(h)
	if err != nil {
		return nil, fmt.Errorf("client: error slurping upload handle to find its length: %v", err)
	}

	c.statsMutex.Lock()
	c.stats.UploadRequests.Blobs++
	c.stats.UploadRequests.Bytes += bodySize
	c.statsMutex.Unlock()

	blobrefStr := h.BlobRef.String()

	// Pre-upload. Check whether the blob already exists on the
	// server and if not, the URL to upload it to.
	pfx, err := c.prefix()
	if err != nil {
		return nil, err
	}
	url_ := fmt.Sprintf("%s/camli/stat", pfx)
	requestBody := "camliversion=1&blob1=" + blobrefStr
	req := c.newRequest("POST", url_)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Body = ioutil.NopCloser(strings.NewReader(requestBody))
	req.ContentLength = int64(len(requestBody))
	req.TransferEncoding = nil

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return errorf("stat http error: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return errorf("stat response had http status %d", resp.StatusCode)
	}

	stat, err := parseStatResponse(resp.Body)
	if err != nil {
		return nil, err
	}
	resp.Body.Close()

	pr := &PutResult{BlobRef: h.BlobRef, Size: bodySize}
	if _, ok := stat.HaveMap[blobrefStr]; ok {
		pr.Skipped = true
		if closer, ok := h.Contents.(io.Closer); ok {
			closer.Close()
		}
		return pr, nil
	}

	pipeReader, pipeWriter := io.Pipe()
	multipartWriter := multipart.NewWriter(pipeWriter)

	copyResult := make(chan error, 1)
	go func() {
		defer pipeWriter.Close()
		part, err := multipartWriter.CreateFormFile(blobrefStr, blobrefStr)
		if err != nil {
			copyResult <- err
			return
		}
		_, err = io.Copy(part, bodyReader)
		if err == nil {
			err = multipartWriter.Close()
		}
		copyResult <- err
	}()

	// TODO(bradfitz): verbosity levels. make this VLOG(2) or something. it's noisy:
	// c.log.Printf("Uploading %s to URL: %s", blobrefStr, stat.uploadUrl)

	req = c.newRequest("POST", stat.uploadUrl)
	req.Header.Set("Content-Type", multipartWriter.FormDataContentType())
	if h.Vivify {
		req.Header.Add("X-Camlistore-Vivify", "1")
	}
	req.Body = ioutil.NopCloser(pipeReader)
	req.ContentLength = multipartOverhead + bodySize + int64(len(blobrefStr))*2
	req.TransferEncoding = nil
	resp, err = c.httpClient.Do(req)
	if err != nil {
		return errorf("upload http error: %v", err)
	}
	defer resp.Body.Close()

	// check error from earlier copy
	if err := <-copyResult; err != nil {
		return errorf("failed to copy contents into multipart writer: %v", err)
	}

	// The only valid HTTP responses are 200 and 303.
	if resp.StatusCode != 200 && resp.StatusCode != 303 {
		return errorf("invalid http response %d in upload response", resp.StatusCode)
	}

	if resp.StatusCode == 303 {
		otherLocation := resp.Header.Get("Location")
		if otherLocation == "" {
			return errorf("303 without a Location")
		}
		baseUrl, _ := url.Parse(stat.uploadUrl)
		absUrl, err := baseUrl.Parse(otherLocation)
		if err != nil {
			return errorf("303 Location URL relative resolve error: %v", err)
		}
		otherLocation = absUrl.String()
		resp, err = http.Get(otherLocation)
		if err != nil {
			return errorf("error following 303 redirect after upload: %v", err)
		}
	}

	ures, err := c.jsonFromResponse("upload", resp)
	if err != nil {
		return errorf("json parse from upload error: %v", err)
	}

	errorText, ok := ures["errorText"].(string)
	if ok {
		c.log.Printf("Blob server reports error: %s", errorText)
	}

	received, ok := ures["received"].([]interface{})
	if !ok {
		return errorf("upload json validity error: no 'received'")
	}

	expectedSize := bodySize

	for _, rit := range received {
		it, ok := rit.(map[string]interface{})
		if !ok {
			return errorf("upload json validity error: 'received' is malformed")
		}
		if it["blobRef"] == blobrefStr {
			switch size := it["size"].(type) {
			case nil:
				return errorf("upload json validity error: 'received' is missing 'size'")
			case float64:
				if int64(size) == expectedSize {
					// Success!
					c.statsMutex.Lock()
					c.stats.Uploads.Blobs++
					c.stats.Uploads.Bytes += expectedSize
					c.statsMutex.Unlock()
					if pr.Size == -1 {
						pr.Size = expectedSize
					}
					return pr, nil
				} else {
					return errorf("Server got blob, but reports wrong length (%v; we sent %d)",
						size, expectedSize)
				}
			default:
				return errorf("unsupported type of 'size' in received response")
			}
		}
	}

	return nil, errors.New("Server didn't receive blob.")
}
<commit_msg>client: minor cleanup<commit_after>/*
Copyright 2011 Google Inc.

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 client

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"camlistore.org/pkg/blobref"
)

var _ = log.Printf

// multipartOverhead is how many extra bytes mime/multipart's
// Writer adds around content
var multipartOverhead = calculateMultipartOverhead()

type UploadHandle struct {
	BlobRef  *blobref.BlobRef
	Size     int64 // or -1 if size isn't known
	Contents io.Reader
	Vivify   bool
}

type PutResult struct {
	BlobRef *blobref.BlobRef
	Size    int64
	Skipped bool // already present on blobserver
}

func (pr *PutResult) SizedBlobRef() blobref.SizedBlobRef {
	return blobref.SizedBlobRef{pr.BlobRef, pr.Size}
}

type statResponse struct {
	HaveMap                    map[string]blobref.SizedBlobRef
	maxUploadSize              int64
	uploadUrl                  string
	uploadUrlExpirationSeconds int
	canLongPoll                bool
}

type ResponseFormatError error

func calculateMultipartOverhead() int64 {
	var b bytes.Buffer
	w := multipart.NewWriter(&b)
	part, _ := w.CreateFormFile("0", "0")

	dummyContents := []byte("0")
	part.Write(dummyContents)

	w.Close()
	return int64(b.Len()) - 3 // remove what was added
}

func newResFormatError(s string, arg ...interface{}) ResponseFormatError {
	return ResponseFormatError(fmt.Errorf(s, arg...))
}

func parseStatResponse(r io.Reader) (sr *statResponse, err error) {
	var (
		ok   bool
		s    = &statResponse{HaveMap: make(map[string]blobref.SizedBlobRef)}
		jmap = make(map[string]interface{})
	)
	if err := json.NewDecoder(io.LimitReader(r, 5<<20)).Decode(&jmap); err != nil {
		return nil, ResponseFormatError(err)
	}
	defer func() {
		if sr == nil {
			log.Printf("parseStatResponse got map: %#v", jmap)
		}
	}()

	s.uploadUrl, ok = jmap["uploadUrl"].(string)
	if !ok {
		return nil, newResFormatError("no 'uploadUrl' in stat response")
	}

	if n, ok := jmap["maxUploadSize"].(float64); ok {
		s.maxUploadSize = int64(n)
	} else {
		return nil, newResFormatError("no 'maxUploadSize' in stat response")
	}

	if n, ok := jmap["uploadUrlExpirationSeconds"].(float64); ok {
		s.uploadUrlExpirationSeconds = int(n)
	} else {
		return nil, newResFormatError("no 'uploadUrlExpirationSeconds' in stat response")
	}

	if v, ok := jmap["canLongPoll"].(bool); ok {
		s.canLongPoll = v
	}

	alreadyHave, ok := jmap["stat"].([]interface{})
	if !ok {
		return nil, newResFormatError("no 'stat' key in stat response")
	}

	for _, li := range alreadyHave {
		m, ok := li.(map[string]interface{})
		if !ok {
			return nil, newResFormatError("'stat' list value of unexpected type %T", li)
		}
		blobRefStr, ok := m["blobRef"].(string)
		if !ok {
			return nil, newResFormatError("'stat' list item has non-string 'blobRef' key")
		}
		size, ok := m["size"].(float64)
		if !ok {
			return nil, newResFormatError("'stat' list item has non-number 'size' key")
		}
		br := blobref.Parse(blobRefStr)
		if br == nil {
			return nil, newResFormatError("'stat' list item has invalid 'blobRef' key")
		}
		s.HaveMap[br.String()] = blobref.SizedBlobRef{br, int64(size)}
	}

	return s, nil
}

func NewUploadHandleFromString(data string) *UploadHandle {
	bref := blobref.SHA1FromString(data)
	r := strings.NewReader(data)
	return &UploadHandle{BlobRef: bref, Size: int64(len(data)), Contents: r}
}

func (c *Client) jsonFromResponse(requestName string, resp *http.Response) (map[string]interface{}, error) {
	if resp.StatusCode != 200 {
		log.Printf("After %s request, failed to JSON from response; status code is %d", requestName, resp.StatusCode)
		io.Copy(os.Stderr, resp.Body)
		return nil, errors.New(fmt.Sprintf("After %s request, HTTP response code is %d; no JSON to parse.", requestName, resp.StatusCode))
	}
	// TODO: LimitReader here for paranoia
	buf := new(bytes.Buffer)
	io.Copy(buf, resp.Body)
	resp.Body.Close()
	jmap := make(map[string]interface{})
	if jerr := json.Unmarshal(buf.Bytes(), &jmap); jerr != nil {
		return nil, jerr
	}
	return jmap, nil
}

func (c *Client) StatBlobs(dest chan<- blobref.SizedBlobRef, blobs []*blobref.BlobRef, wait time.Duration) error {
	if len(blobs) == 0 {
		return nil
	}

	// TODO: if len(blobs) > 1000 or something, cut this up into
	// multiple http requests, and also if the server returns a
	// 400 error, per the blob-stat-protocol.txt document.
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "camliversion=1")
	for n, blob := range blobs {
		if blob == nil {
			panic("nil blob")
		}
		fmt.Fprintf(&buf, "&blob%d=%s", n+1, blob)
	}

	if wait > 0 {
		secs := int(wait.Seconds())
		if secs == 0 {
			secs = 1
		}
		fmt.Fprintf(&buf, "&maxwaitsec=%d", secs)
	}

	pfx, err := c.prefix()
	if err != nil {
		return err
	}
	req := c.newRequest("POST", fmt.Sprintf("%s/camli/stat", pfx))
	bodyStr := buf.String()
	req.Body = ioutil.NopCloser(strings.NewReader(bodyStr))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.ContentLength = int64(len(bodyStr))

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("stat HTTP error: %v", err)
	}
	if resp.Body != nil {
		defer resp.Body.Close()
	}

	if resp.StatusCode != 200 {
		return fmt.Errorf("stat response had http status %d", resp.StatusCode)
	}

	stat, err := parseStatResponse(resp.Body)
	if err != nil {
		return err
	}

	for _, sb := range stat.HaveMap {
		dest <- sb
	}
	return nil
}

// Figure out the size of the contents.
// If the size was provided, trust it.
// If the size was not provided (-1), slurp.
func readerAndSize(h *UploadHandle) (io.Reader, int64, error) {
	if h.Size != -1 {
		return h.Contents, h.Size, nil
	}
	var b bytes.Buffer
	n, err := io.Copy(&b, h.Contents)
	if err != nil {
		return nil, 0, err
	}
	return &b, n, nil
}

func (c *Client) Upload(h *UploadHandle) (*PutResult, error) {
	errorf := func(msg string, arg ...interface{}) (*PutResult, error) {
		err := fmt.Errorf(msg, arg...)
		c.log.Print(err.Error())
		return nil, err
	}

	bodyReader, bodySize, err := readerAndSize(h)
	if err != nil {
		return nil, fmt.Errorf("client: error slurping upload handle to find its length: %v", err)
	}

	c.statsMutex.Lock()
	c.stats.UploadRequests.Blobs++
	c.stats.UploadRequests.Bytes += bodySize
	c.statsMutex.Unlock()

	blobrefStr := h.BlobRef.String()

	// Pre-upload. Check whether the blob already exists on the
	// server and if not, the URL to upload it to.
	pfx, err := c.prefix()
	if err != nil {
		return nil, err
	}
	url_ := fmt.Sprintf("%s/camli/stat", pfx)
	requestBody := "camliversion=1&blob1=" + blobrefStr
	req := c.newRequest("POST", url_)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Body = ioutil.NopCloser(strings.NewReader(requestBody))
	req.ContentLength = int64(len(requestBody))
	req.TransferEncoding = nil

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return errorf("stat http error: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return errorf("stat response had http status %d", resp.StatusCode)
	}

	stat, err := parseStatResponse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, err
	}

	pr := &PutResult{BlobRef: h.BlobRef, Size: bodySize}
	if _, ok := stat.HaveMap[blobrefStr]; ok {
		pr.Skipped = true
		if closer, ok := h.Contents.(io.Closer); ok {
			closer.Close()
		}
		return pr, nil
	}

	pipeReader, pipeWriter := io.Pipe()
	multipartWriter := multipart.NewWriter(pipeWriter)

	copyResult := make(chan error, 1)
	go func() {
		defer pipeWriter.Close()
		part, err := multipartWriter.CreateFormFile(blobrefStr, blobrefStr)
		if err != nil {
			copyResult <- err
			return
		}
		_, err = io.Copy(part, bodyReader)
		if err == nil {
			err = multipartWriter.Close()
		}
		copyResult <- err
	}()

	// TODO(bradfitz): verbosity levels. make this VLOG(2) or something. it's noisy:
	// c.log.Printf("Uploading %s to URL: %s", blobrefStr, stat.uploadUrl)

	req = c.newRequest("POST", stat.uploadUrl)
	req.Header.Set("Content-Type", multipartWriter.FormDataContentType())
	if h.Vivify {
		req.Header.Add("X-Camlistore-Vivify", "1")
	}
	req.Body = ioutil.NopCloser(pipeReader)
	req.ContentLength = multipartOverhead + bodySize + int64(len(blobrefStr))*2
	req.TransferEncoding = nil
	resp, err = c.httpClient.Do(req)
	if err != nil {
		return errorf("upload http error: %v", err)
	}
	defer resp.Body.Close()

	// check error from earlier copy
	if err := <-copyResult; err != nil {
		return errorf("failed to copy contents into multipart writer: %v", err)
	}

	// The only valid HTTP responses are 200 and 303.
	if resp.StatusCode != 200 && resp.StatusCode != 303 {
		return errorf("invalid http response %d in upload response", resp.StatusCode)
	}

	if resp.StatusCode == 303 {
		otherLocation := resp.Header.Get("Location")
		if otherLocation == "" {
			return errorf("303 without a Location")
		}
		baseUrl, _ := url.Parse(stat.uploadUrl)
		absUrl, err := baseUrl.Parse(otherLocation)
		if err != nil {
			return errorf("303 Location URL relative resolve error: %v", err)
		}
		otherLocation = absUrl.String()
		resp, err = http.Get(otherLocation)
		if err != nil {
			return errorf("error following 303 redirect after upload: %v", err)
		}
	}

	ures, err := c.jsonFromResponse("upload", resp)
	if err != nil {
		return errorf("json parse from upload error: %v", err)
	}

	errorText, ok := ures["errorText"].(string)
	if ok {
		c.log.Printf("Blob server reports error: %s", errorText)
	}

	received, ok := ures["received"].([]interface{})
	if !ok {
		return errorf("upload json validity error: no 'received'")
	}

	expectedSize := bodySize

	for _, rit := range received {
		it, ok := rit.(map[string]interface{})
		if !ok {
			return errorf("upload json validity error: 'received' is malformed")
		}
		if it["blobRef"] == blobrefStr {
			switch size := it["size"].(type) {
			case nil:
				return errorf("upload json validity error: 'received' is missing 'size'")
			case float64:
				if int64(size) == expectedSize {
					// Success!
					c.statsMutex.Lock()
					c.stats.Uploads.Blobs++
					c.stats.Uploads.Bytes += expectedSize
					c.statsMutex.Unlock()
					if pr.Size == -1 {
						pr.Size = expectedSize
					}
					return pr, nil
				} else {
					return errorf("Server got blob, but reports wrong length (%v; we sent %d)",
						size, expectedSize)
				}
			default:
				return errorf("unsupported type of 'size' in received response")
			}
		}
	}

	return nil, errors.New("Server didn't receive blob.")
}
<|endoftext|>


<commit_before>/*
 * Copyright 2018 The original author or authors
 *
 * 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 core

import (
	"context"
	"errors"
	"fmt"
	"github.com/boz/go-logutil"
	"github.com/boz/kail"
	"github.com/boz/kcache/types/pod"
	build "github.com/knative/build/pkg/apis/build/v1alpha1"
	"github.com/knative/serving/pkg/apis/serving/v1alpha1"
	"io"
	"io/ioutil"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"log"
	"strconv"
	"strings"
	"time"
)

const functionLabel = "riff.projectriff.io/function"
const buildAnnotation = "riff.projectriff.io/nonce"

type CreateFunctionOptions struct {
	CreateServiceOptions

	GitRepo     string
	GitRevision string

	InvokerURL string
	Handler    string
	Artifact   string
}

func (c *client) CreateFunction(options CreateFunctionOptions, log io.Writer) (*v1alpha1.Service, error) {
	ns := c.explicitOrConfigNamespace(options.Namespaced)

	s, err := newService(options.CreateServiceOptions)
	if err != nil {
		return nil, err
	}

	labels := s.Spec.RunLatest.Configuration.RevisionTemplate.Labels
	if labels == nil {
		labels = map[string]string{}
	}
	labels[functionLabel] = options.Name
	s.Spec.RunLatest.Configuration.RevisionTemplate.SetLabels(labels)
	annotations := s.Spec.RunLatest.Configuration.RevisionTemplate.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	annotations[buildAnnotation] = "1"
	s.Spec.RunLatest.Configuration.RevisionTemplate.SetAnnotations(annotations)

	s.Spec.RunLatest.Configuration.Build = &build.BuildSpec{
		ServiceAccountName: "riff-build",
		Source: &build.SourceSpec{
			Git: &build.GitSourceSpec{
				Url:      options.GitRepo,
				Revision: options.GitRevision,
			},
		},
		Template: &build.TemplateInstantiationSpec{
			Name: "riff",
			Arguments: []build.ArgumentSpec{
				{Name: "IMAGE", Value: options.Image},
				{Name: "INVOKER_PATH", Value: options.InvokerURL},
				{Name: "FUNCTION_ARTIFACT", Value: options.Artifact},
				{Name: "FUNCTION_HANDLER", Value: options.Handler},
				{Name: "FUNCTION_NAME", Value: options.Name},
			},
		},
	}

	if !options.DryRun {
		_, err := c.serving.ServingV1alpha1().Services(ns).Create(s)
		if err != nil {
			return nil, err
		}

		if options.Verbose || options.Wait {
			stopChan := make(chan struct{})
			errChan := make(chan error)
			if options.Verbose {
				go c.displayFunctionCreationProgress(ns, s.Name, log, stopChan, errChan)
			}
			err := c.waitForSuccessOrFailure(ns, s.Name, stopChan, errChan)
			if err != nil {
				return nil, err
			}
		}
	}

	return s, nil
}

func (c *client) displayFunctionCreationProgress(serviceNamespace string, serviceName string, logWriter io.Writer, stopChan <-chan struct{}, errChan chan<- error) {
	time.Sleep(1000 * time.Millisecond) // ToDo: need some time for revision to get created - is there a better way to slow this down?
	revName, err := c.revisionName(serviceNamespace, serviceName, logWriter, stopChan)
	if err != nil {
		errChan <- err
		return
	} else if revName == "" { // stopped
		return
	}

	ctx := newContext()

	podController, err := c.podController(revName, serviceName, ctx)
	if err != nil {
		errChan <- err
		return
	}

	config, err := c.clientConfig.ClientConfig()
	if err != nil {
		errChan <- err
		return
	}

	controller, err := kail.NewController(ctx, c.kubeClient, config, podController, kail.NewContainerFilter([]string{}), time.Hour)
	if err != nil {
		errChan <- err
		return
	}

	streamLogs(logWriter, controller, stopChan)
	close(errChan)
}

func (c *client) revisionName(serviceNamespace string, serviceName string, logWriter io.Writer, stopChan <-chan struct{}) (string, error) {
	fmt.Fprintf(logWriter, "Waiting for LatestCreatedRevisionName:")
	revName := ""
	for {
		serviceObj, err := c.serving.ServingV1alpha1().Services(serviceNamespace).Get(serviceName, v1.GetOptions{})
		if err != nil {
			return "", err
		}
		revName = serviceObj.Status.LatestCreatedRevisionName
		if revName != "" {
			break
		}
		time.Sleep(1000 * time.Millisecond)
		fmt.Fprintf(logWriter, ".")
		select {
		case <-stopChan:
			return "", nil
		default:
			// continue
		}
	}
	fmt.Fprintf(logWriter, " %s\n", revName)
	return revName, nil
}

func newContext() context.Context {
	ctx := context.Background()
	// avoid kail logs appearing
	l := logutil.New(log.New(ioutil.Discard, "", log.LstdFlags), ioutil.Discard)
	ctx = logutil.NewContext(ctx, l)
	return ctx
}

func (c *client) podController(revName string, serviceName string, ctx context.Context) (pod.Controller, error) {
	dsb := kail.NewDSBuilder()

	buildSelOld, err := labels.Parse(fmt.Sprintf("%s=%s", "build-name", revName))
	if err != nil {
		return nil, err
	}
	buildSel, err := labels.Parse(fmt.Sprintf("%s=%s", "build.knative.dev/buildName", revName))
	if err != nil {
		return nil, err
	}
	runtimeSel, err := labels.Parse(fmt.Sprintf("%s=%s", "serving.knative.dev/configuration", serviceName))
	if err != nil {
		return nil, err
	}
	ds, err := dsb.WithSelectors(or(buildSel, runtimeSel, buildSelOld)).Create(ctx, c.kubeClient) // delete buildSelOld when https://github.com/knative/build/pull/299 is integrated into k/s
	if err != nil {
		return nil, err
	}

	return ds.Pods(), nil
}

func streamLogs(log io.Writer, controller kail.Controller, stopChan <-chan struct{}) {
	events := controller.Events()
	done := controller.Done()
	writer := NewWriter(log)
	for {
		select {
		case ev := <-events:
			// filter out sidecar logs
			container := ev.Source().Container()
			switch container {
			case "queue-proxy":
			case "istio-init":
			case "istio-proxy":
			default:
				writer.Print(ev)
			}
		case <-done:
			return
		case <-stopChan:
			return
		}
	}
}

func (c *client) waitForSuccessOrFailure(namespace string, name string, stopChan chan<- struct{}, errChan <-chan error) error {
	defer close(stopChan)
	for i := 0; i >= 0; i++ {
		select {
		case err := <-errChan:
			return err
		default:
		}
		time.Sleep(500 * time.Millisecond)
		serviceStatusOptions := ServiceStatusOptions{
			Namespaced: Namespaced{namespace},
			Name:       name,
		}
		cond, err := c.ServiceStatus(serviceStatusOptions)
		if err != nil {
			// allow some time for service status to show up
			if i < 20 {
				continue
			}
			return fmt.Errorf("waitForSuccessOrFailure failed to obtain service status: %v", err)
		}

		switch cond.Status {
		case corev1.ConditionTrue:
			return nil
		case corev1.ConditionFalse:
			var message string
			conds, err := c.ServiceConditions(serviceStatusOptions)
			if err != nil {
				// fall back to a basic message
				message = cond.Message
			} else {
				message = serviceConditionsMessage(conds, cond.Message)
			}
			return fmt.Errorf("function create failed: %s: %s", cond.Reason, message)
		default:
			// keep going until outcome is known
		}
	}
	return nil
}

func serviceConditionsMessage(conds []v1alpha1.ServiceCondition, primaryMessage string) string {
	msg := []string{primaryMessage}
	for _, cond := range conds {
		if cond.Status == corev1.ConditionFalse && cond.Type != v1alpha1.ServiceConditionReady && cond.Message != primaryMessage {
			msg = append(msg, cond.Message)
		}
	}
	return strings.Join(msg, "; ")
}

func or(disjuncts ...labels.Selector) labels.Selector {
	return selectorDisjunction(disjuncts)
}

type selectorDisjunction []labels.Selector

func (selectorDisjunction) Add(r ...labels.Requirement) labels.Selector {
	panic("implement me")
}

func (selectorDisjunction) DeepCopySelector() labels.Selector {
	panic("implement me")
}

func (selectorDisjunction) Empty() bool {
	panic("implement me")
}

func (sd selectorDisjunction) Matches(lbls labels.Labels) bool {
	for _, s := range sd {
		if s.Matches(lbls) {
			return true
		}
	}
	return false
}

func (selectorDisjunction) Requirements() (requirements labels.Requirements, selectable bool) {
	panic("implement me")
}

func (selectorDisjunction) String() string {
	panic("implement me")
}

type BuildFunctionOptions struct {
	Namespaced
	Name    string
	Verbose bool
}

func (c *client) BuildFunction(options BuildFunctionOptions, log io.Writer) error {
	ns := c.explicitOrConfigNamespace(options.Namespaced)

	s, err := c.service(options.Namespaced, options.Name)
	if err != nil {
		return err
	}

	labels := s.Spec.RunLatest.Configuration.RevisionTemplate.Labels
	if labels[functionLabel] == "" {
		return errors.New(fmt.Sprintf("the service named \"%s\" is not a riff function", options.Name))
	}

	annotations := s.Spec.RunLatest.Configuration.RevisionTemplate.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	build := annotations[buildAnnotation]
	i, err := strconv.Atoi(build)
	if err != nil {
		i = 0
	}
	annotations[buildAnnotation] = strconv.Itoa(i + 1)
	s.Spec.RunLatest.Configuration.RevisionTemplate.SetAnnotations(annotations)

	_, err = c.serving.ServingV1alpha1().Services(s.Namespace).Update(s)
	if err != nil {
		return err
	}

	if options.Verbose {
		stopChan := make(chan struct{})
		errChan := make(chan error)
		go c.displayFunctionCreationProgress(ns, s.Name, log, stopChan, errChan)
		err := c.waitForSuccessOrFailure(ns, s.Name, stopChan, errChan)
		if err != nil {
			return err
		}
	}

	return nil
}
<commit_msg>Fix verbose output for failed function builds<commit_after>/*
 * Copyright 2018 The original author or authors
 *
 * 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 core

import (
	"context"
	"errors"
	"fmt"
	"github.com/boz/go-logutil"
	"github.com/boz/kail"
	"github.com/boz/kcache/types/pod"
	build "github.com/knative/build/pkg/apis/build/v1alpha1"
	"github.com/knative/serving/pkg/apis/serving/v1alpha1"
	"io"
	"io/ioutil"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"log"
	"strconv"
	"strings"
	"time"
)

const functionLabel = "riff.projectriff.io/function"
const buildAnnotation = "riff.projectriff.io/nonce"

type CreateFunctionOptions struct {
	CreateServiceOptions

	GitRepo     string
	GitRevision string

	InvokerURL string
	Handler    string
	Artifact   string
}

func (c *client) CreateFunction(options CreateFunctionOptions, log io.Writer) (*v1alpha1.Service, error) {
	ns := c.explicitOrConfigNamespace(options.Namespaced)

	s, err := newService(options.CreateServiceOptions)
	if err != nil {
		return nil, err
	}

	labels := s.Spec.RunLatest.Configuration.RevisionTemplate.Labels
	if labels == nil {
		labels = map[string]string{}
	}
	labels[functionLabel] = options.Name
	s.Spec.RunLatest.Configuration.RevisionTemplate.SetLabels(labels)
	annotations := s.Spec.RunLatest.Configuration.RevisionTemplate.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	annotations[buildAnnotation] = "1"
	s.Spec.RunLatest.Configuration.RevisionTemplate.SetAnnotations(annotations)

	s.Spec.RunLatest.Configuration.Build = &build.BuildSpec{
		ServiceAccountName: "riff-build",
		Source: &build.SourceSpec{
			Git: &build.GitSourceSpec{
				Url:      options.GitRepo,
				Revision: options.GitRevision,
			},
		},
		Template: &build.TemplateInstantiationSpec{
			Name: "riff",
			Arguments: []build.ArgumentSpec{
				{Name: "IMAGE", Value: options.Image},
				{Name: "INVOKER_PATH", Value: options.InvokerURL},
				{Name: "FUNCTION_ARTIFACT", Value: options.Artifact},
				{Name: "FUNCTION_HANDLER", Value: options.Handler},
				{Name: "FUNCTION_NAME", Value: options.Name},
			},
		},
	}

	if !options.DryRun {
		_, err := c.serving.ServingV1alpha1().Services(ns).Create(s)
		if err != nil {
			return nil, err
		}

		if options.Verbose || options.Wait {
			stopChan := make(chan struct{})
			errChan := make(chan error)
			if options.Verbose {
				go c.displayFunctionCreationProgress(ns, s.Name, log, stopChan, errChan)
			}
			err := c.waitForSuccessOrFailure(ns, s.Name, 1, stopChan, errChan)
			if err != nil {
				return nil, err
			}
		}
	}

	return s, nil
}

func (c *client) displayFunctionCreationProgress(serviceNamespace string, serviceName string, logWriter io.Writer, stopChan <-chan struct{}, errChan chan<- error) {
	revName, err := c.revisionName(serviceNamespace, serviceName, logWriter, stopChan)
	if err != nil {
		errChan <- err
		return
	} else if revName == "" { // stopped
		return
	}

	ctx := newContext()

	podController, err := c.podController(revName, serviceName, ctx)
	if err != nil {
		errChan <- err
		return
	}

	config, err := c.clientConfig.ClientConfig()
	if err != nil {
		errChan <- err
		return
	}

	controller, err := kail.NewController(ctx, c.kubeClient, config, podController, kail.NewContainerFilter([]string{}), time.Hour)
	if err != nil {
		errChan <- err
		return
	}

	streamLogs(logWriter, controller, stopChan)
	close(errChan)
}

func (c *client) revisionName(serviceNamespace string, serviceName string, logWriter io.Writer, stopChan <-chan struct{}) (string, error) {
	fmt.Fprintf(logWriter, "Waiting for LatestCreatedRevisionName:")
	revName := ""
	for {
		serviceObj, err := c.serving.ServingV1alpha1().Services(serviceNamespace).Get(serviceName, v1.GetOptions{})
		if err != nil {
			return "", err
		}
		revName = serviceObj.Status.LatestCreatedRevisionName
		if revName != "" {
			break
		}
		time.Sleep(1000 * time.Millisecond)
		fmt.Fprintf(logWriter, ".")
		select {
		case <-stopChan:
			return "", nil
		default:
			// continue
		}
	}
	fmt.Fprintf(logWriter, " %s\n", revName)
	return revName, nil
}

func newContext() context.Context {
	ctx := context.Background()
	// avoid kail logs appearing
	l := logutil.New(log.New(ioutil.Discard, "", log.LstdFlags), ioutil.Discard)
	ctx = logutil.NewContext(ctx, l)
	return ctx
}

func (c *client) podController(revName string, serviceName string, ctx context.Context) (pod.Controller, error) {
	dsb := kail.NewDSBuilder()

	buildSelOld, err := labels.Parse(fmt.Sprintf("%s=%s", "build-name", revName))
	if err != nil {
		return nil, err
	}
	buildSel, err := labels.Parse(fmt.Sprintf("%s=%s", "build.knative.dev/buildName", revName))
	if err != nil {
		return nil, err
	}
	runtimeSel, err := labels.Parse(fmt.Sprintf("%s=%s", "serving.knative.dev/configuration", serviceName))
	if err != nil {
		return nil, err
	}
	ds, err := dsb.WithSelectors(or(buildSel, runtimeSel, buildSelOld)).Create(ctx, c.kubeClient) // delete buildSelOld when https://github.com/knative/build/pull/299 is integrated into k/s
	if err != nil {
		return nil, err
	}

	return ds.Pods(), nil
}

func streamLogs(log io.Writer, controller kail.Controller, stopChan <-chan struct{}) {
	events := controller.Events()
	done := controller.Done()
	writer := NewWriter(log)
	for {
		select {
		case ev := <-events:
			// filter out sidecar logs
			container := ev.Source().Container()
			switch container {
			case "queue-proxy":
			case "istio-init":
			case "istio-proxy":
			default:
				writer.Print(ev)
			}
		case <-done:
			return
		case <-stopChan:
			return
		}
	}
}

func (c *client) waitForSuccessOrFailure(namespace string, name string, gen int64, stopChan chan<- struct{}, errChan <-chan error) error {
	defer close(stopChan)
	for i := 0; i >= 0; i++ {
		select {
		case err := <-errChan:
			return err
		default:
		}
		time.Sleep(500 * time.Millisecond)
		service, err := c.service( Namespaced{namespace}, name)
		if err != nil {
			return fmt.Errorf("waitForSuccessOrFailure failed to obtain service: %v", err)
		}
		if service.Status.ObservedGeneration < gen {
			// allow some time for service status observed generation to show up
			if i < 20 {
				continue
			}
			return fmt.Errorf("waitForSuccessOrFailure failed to obtain service status for observedGeneration %d: %v", gen, err)
		}
		serviceStatusOptions := ServiceStatusOptions{
			Namespaced: Namespaced{namespace},
			Name:       name,
		}
		cond, err := c.ServiceStatus(serviceStatusOptions)
		if err != nil {
			return fmt.Errorf("waitForSuccessOrFailure failed to obtain service status: %v", err)
		}

		switch cond.Status {
		case corev1.ConditionTrue:
			return nil
		case corev1.ConditionFalse:
			someStateIsUnknown := false
			conds, err := c.ServiceConditions(serviceStatusOptions)
			if err == nil {
				for _, c := range conds {
					if c.Status == corev1.ConditionUnknown {
						someStateIsUnknown = true
						break
					}
				}
			}
			if !someStateIsUnknown {
				var message string
				if err != nil {
					// fall back to a basic message
					message = cond.Message
				} else {
					message = serviceConditionsMessage(conds, cond.Message)
				}
				return fmt.Errorf("function creation failed: %s: %s", cond.Reason, message)
			}
		default:
			// keep going until outcome is known
		}
	}
	return nil
}

func serviceConditionsMessage(conds []v1alpha1.ServiceCondition, primaryMessage string) string {
	msg := []string{primaryMessage}
	for _, cond := range conds {
		if cond.Status == corev1.ConditionFalse && cond.Type != v1alpha1.ServiceConditionReady && cond.Message != primaryMessage {
			msg = append(msg, cond.Message)
		}
	}
	return strings.Join(msg, "; ")
}

func or(disjuncts ...labels.Selector) labels.Selector {
	return selectorDisjunction(disjuncts)
}

type selectorDisjunction []labels.Selector

func (selectorDisjunction) Add(r ...labels.Requirement) labels.Selector {
	panic("implement me")
}

func (selectorDisjunction) DeepCopySelector() labels.Selector {
	panic("implement me")
}

func (selectorDisjunction) Empty() bool {
	panic("implement me")
}

func (sd selectorDisjunction) Matches(lbls labels.Labels) bool {
	for _, s := range sd {
		if s.Matches(lbls) {
			return true
		}
	}
	return false
}

func (selectorDisjunction) Requirements() (requirements labels.Requirements, selectable bool) {
	panic("implement me")
}

func (selectorDisjunction) String() string {
	panic("implement me")
}

type BuildFunctionOptions struct {
	Namespaced
	Name    string
	Verbose bool
}

func (c *client) getServiceSpecGeneration(namespaced Namespaced, name string) (int64, error) {
	s, err := c.service(namespaced, name)
	if err != nil {
		return 0, err
	}
	return s.Spec.Generation, nil
}

func (c *client) BuildFunction(options BuildFunctionOptions, log io.Writer) error {
	ns := c.explicitOrConfigNamespace(options.Namespaced)

	s, err := c.service(options.Namespaced, options.Name)
	if err != nil {
		return err
	}

	gen := s.Spec.Generation

	labels := s.Spec.RunLatest.Configuration.RevisionTemplate.Labels
	if labels[functionLabel] == "" {
		return errors.New(fmt.Sprintf("the service named \"%s\" is not a riff function", options.Name))
	}

	annotations := s.Spec.RunLatest.Configuration.RevisionTemplate.Annotations
	if annotations == nil {
		annotations = map[string]string{}
	}
	build := annotations[buildAnnotation]
	i, err := strconv.Atoi(build)
	if err != nil {
		i = 0
	}
	annotations[buildAnnotation] = strconv.Itoa(i + 1)
	s.Spec.RunLatest.Configuration.RevisionTemplate.SetAnnotations(annotations)

	_, err = c.serving.ServingV1alpha1().Services(s.Namespace).Update(s)
	if err != nil {
		return err
	}

	if options.Verbose {
		stopChan := make(chan struct{})
		errChan := make(chan error)
		var (
			nextGen int64
			err error
		)
		for i := 0; i < 10; i++ {
			if i >= 10 {
				return fmt.Errorf("build unsuccesful for \"%s\", service resource was never updated", options.Name)
			}
			time.Sleep(500 * time.Millisecond)
			nextGen, err = c.getServiceSpecGeneration(options.Namespaced, options.Name)
			if err != nil {
				return err
			}
			if nextGen > gen {
				break
			}
		}
		go c.displayFunctionCreationProgress(ns, s.Name, log, stopChan, errChan)
		err = c.waitForSuccessOrFailure(ns, s.Name, nextGen, stopChan, errChan)
		if err != nil {
			return err
		}
	}

	return nil
}
<|endoftext|>


<commit_before>package image

import (
	"path"
	"strings"

	"github.com/rancher/rancher/pkg/settings"
)

func Resolve(image string) string {
	reg := settings.SystemDefaultRegistry.Get()
	if reg != "" && !strings.HasPrefix(image, reg) {
		return path.Join(reg, image)
	}

	return image
}
<commit_msg>Add `rancher/` prefix for images from dockerhub library<commit_after>package image

import (
	"path"
	"strings"

	"github.com/rancher/rancher/pkg/settings"
)

func Resolve(image string) string {
	reg := settings.SystemDefaultRegistry.Get()
	if reg != "" && !strings.HasPrefix(image, reg) {
		//Images from Dockerhub Library repo, we add rancher prefix when using private registry
		if !strings.Contains(image, "/") {
			image = "rancher/" + image
		}
		return path.Join(reg, image)
	}

	return image
}
<|endoftext|>


<commit_before>package kubeutil

import (
	"fmt"

	"k8s.io/client-go/pkg/api/v1"
)

const (
	ErrImagePullReason = "ErrImagePull"
	ErrorReason        = "Error"
)

func PodWarnings(pod *v1.Pod) error {
	for _, cs := range pod.Status.InitContainerStatuses {
		err := containerWarnings(cs)
		if err != nil {
			return err
		}
	}

	for _, cs := range pod.Status.ContainerStatuses {
		err := containerWarnings(cs)
		if err != nil {
			return err
		}
	}

	return nil
}

type ErrImagePull struct{}

func (err ErrImagePull) Error() string {
	return "failed to pull docker image"
}

type ErrCrash struct {
	Name         string
	ExitCode     int32
	RestartCount int32
}

func (err ErrCrash) Error() string {
	return fmt.Sprintf(
		"failed to start container (Container: %v, ExitCode: %v, Restarts: %v)",
		err.Name, err.ExitCode, err.RestartCount)
}

func containerWarnings(status v1.ContainerStatus) error {
	if status.State.Waiting != nil && status.State.Waiting.Reason == ErrImagePullReason {
		return ErrImagePull{}
	}

	if status.State.Terminated != nil && status.State.Terminated.Reason == ErrorReason {
		return ErrCrash{
			Name:         status.Name,
			ExitCode:     status.State.Terminated.ExitCode,
			RestartCount: status.RestartCount,
		}
	}

	return nil
}
<commit_msg>fix annoying pod restart notifications<commit_after>package kubeutil

import (
	"fmt"

	"k8s.io/client-go/pkg/api/v1"
)

const (
	ErrImagePullReason = "ErrImagePull"
	ErrorReason        = "Error"
)

func PodWarnings(pod *v1.Pod) error {
	if pod.ObjectMeta.DeletionTimestamp != nil {
		// ignore Pods with pending deletion
		return nil
	}

	for _, cs := range pod.Status.InitContainerStatuses {
		err := containerWarnings(cs)
		if err != nil {
			return err
		}
	}

	for _, cs := range pod.Status.ContainerStatuses {
		err := containerWarnings(cs)
		if err != nil {
			return err
		}
	}

	return nil
}

type ErrImagePull struct{}

func (err ErrImagePull) Error() string {
	return "failed to pull docker image"
}

type ErrCrash struct {
	Name         string
	ExitCode     int32
	RestartCount int32
}

func (err ErrCrash) Error() string {
	return fmt.Sprintf(
		"failed to start container (Container: %v, ExitCode: %v, Restarts: %v)",
		err.Name, err.ExitCode, err.RestartCount)
}

func containerWarnings(status v1.ContainerStatus) error {
	if status.State.Waiting != nil && status.State.Waiting.Reason == ErrImagePullReason {
		return ErrImagePull{}
	}

	if status.State.Terminated != nil && status.State.Terminated.Reason == ErrorReason {
		return ErrCrash{
			Name:         status.Name,
			ExitCode:     status.State.Terminated.ExitCode,
			RestartCount: status.RestartCount,
		}
	}

	return nil
}
<|endoftext|>