diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Jenkinsfile b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Jenkinsfile index f01fbb07b4d6..11cfc2b1e9b3 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Jenkinsfile +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Jenkinsfile @@ -24,6 +24,8 @@ limitations under the License. // TEST_ZONE: GCP Zone in which to create test GKE cluster // TEST_ACCOUNT: GCP service account credentials (JSON file) to use for testing. +def repo_url = params.REPO_URL + def updatePullRequest(flow, success = false) { def state, message switch (flow) { @@ -39,10 +41,14 @@ def updatePullRequest(flow, success = false) { default: error('flow can only be run or verify') } - setGitHubPullRequestStatus( - context: env.JOB_NAME, - message: message, - state: state) + + step([ + $class: "GitHubCommitStatusSetter", + reposSource: [$class: "ManuallyEnteredRepositorySource", url: "${repo_url}"], + contextSource: [$class: "ManuallyEnteredCommitContextSource", context: "${JOB_NAME}"], + errorHandlers: [[$class: "ChangingBuildStatusErrorHandler", result: "UNSTABLE"]], + statusResultSource: [ $class: "ConditionalStatusResultSource", results: [[$class: "AnyBuildResult", message: message, state: state]] ] + ]); } // Verify required parameters diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Makefile b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Makefile index 6916a3064790..b647a384cb53 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Makefile +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/Makefile @@ -249,6 +249,9 @@ test-integration: .init $(scBuildImageTarget) build # golang integration tests $(DOCKER_CMD) test/integration.sh +clean-e2e: + rm -f $(BINDIR)/e2e.test + test-e2e: .generate_files $(BINDIR)/e2e.test $(BINDIR)/e2e.test @@ -292,26 +295,24 @@ clean-coverage: images: user-broker-image \ controller-manager-image apiserver-image +define build-and-tag # (service, image, mutable_image, prefix) + $(eval build_path := "$(4)build/$(1)") + $(eval tmp_build_path := "$(build_path)/tmp") + mkdir -p $(tmp_build_path) + cp $(BINDIR)/$(1) $(tmp_build_path) + docker build -t $(2) $(build_path) + docker tag $(2) $(3) + rm -rf $(tmp_build_path) +endef + user-broker-image: contrib/build/user-broker/Dockerfile $(BINDIR)/user-broker - mkdir -p contrib/build/user-broker/tmp - cp $(BINDIR)/user-broker contrib/build/user-broker/tmp - docker build -t $(USER_BROKER_IMAGE) contrib/build/user-broker - docker tag $(USER_BROKER_IMAGE) $(USER_BROKER_MUTABLE_IMAGE) - rm -rf contrib/build/user-broker/tmp + $(call build-and-tag,"user-broker",$(USER_BROKER_IMAGE),$(USER_BROKER_MUTABLE_IMAGE),"contrib/") apiserver-image: build/apiserver/Dockerfile $(BINDIR)/apiserver - mkdir -p build/apiserver/tmp - cp $(BINDIR)/apiserver build/apiserver/tmp - docker build -t $(APISERVER_IMAGE) build/apiserver - docker tag $(APISERVER_IMAGE) $(APISERVER_MUTABLE_IMAGE) - rm -rf build/apiserver/tmp + $(call build-and-tag,"apiserver",$(APISERVER_IMAGE),$(APISERVER_MUTABLE_IMAGE)) controller-manager-image: build/controller-manager/Dockerfile $(BINDIR)/controller-manager - mkdir -p build/controller-manager/tmp - cp $(BINDIR)/controller-manager build/controller-manager/tmp - docker build -t $(CONTROLLER_MANAGER_IMAGE) build/controller-manager - docker tag $(CONTROLLER_MANAGER_IMAGE) $(CONTROLLER_MANAGER_MUTABLE_IMAGE) - rm -rf build/controller-manager/tmp + $(call build-and-tag,"controller-manager",$(CONTROLLER_MANAGER_IMAGE),$(CONTROLLER_MANAGER_MUTABLE_IMAGE)) # Push our Docker Images to a registry ###################################### diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/README.md b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/README.md index e78246ddd18c..7a7b3acd112d 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/README.md +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/README.md @@ -1,6 +1,7 @@ ## `service-catalog` -[![Build Status](https://travis-ci.org/kubernetes-incubator/service-catalog.svg?branch=master)](https://travis-ci.org/kubernetes-incubator/service-catalog) +[![Build Status](https://travis-ci.org/kubernetes-incubator/service-catalog.svg?branch=master)](https://travis-ci.org/kubernetes-incubator/service-catalog "Travis") +[![Build Status](https://service-catalog-jenkins.appspot.com/buildStatus/icon?job=service-catalog-master-testing)](https://service-catalog-jenkins.appspot.com/job/service-catalog-master-testing/ "Jenkins") ### Introduction diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/apiserver-deployment.yaml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/apiserver-deployment.yaml index 9ac0288c40ac..3d7ca00184c3 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/apiserver-deployment.yaml +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/apiserver-deployment.yaml @@ -32,6 +32,10 @@ spec: cpu: 100m memory: 30Mi args: + {{ if .Values.apiserver.audit.activated -}} + - --audit-log-path + - {{ .Values.apiserver.audit.logPath }} + {{- end}} - --admission-control - "KubernetesNamespaceLifecycle" - --secure-port diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/controller-manager-deployment.yaml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/controller-manager-deployment.yaml index f200c95eb579..24066af4a9b3 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/controller-manager-deployment.yaml +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/templates/controller-manager-deployment.yaml @@ -31,9 +31,23 @@ spec: limits: cpu: 100m memory: 30Mi + env: + - name: K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace args: - --port - "8080" + {{ if .Values.controllerManager.leaderElectionNamespace.activated -}} + - "--leader-election-namespace=$(K8S_NAMESPACE)" + {{- end }} + {{ if .Values.controllerManager.profiling.disabled -}} + - "--profiling=false" + {{- end}} + {{ if .Values.controllerManager.profiling.contentionProfiling -}} + - "--contention-profiling=true" + {{- end}} {{- if not .Values.useAggregator }} - --service-catalog-api-server-url {{- if .Values.apiserver.insecure }} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/values.yaml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/values.yaml index fff4b97a5fe8..0b083810a988 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/values.yaml +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/catalog/values.yaml @@ -1,7 +1,7 @@ # Default values for Service Catalog apiserver: # apiserver image to use - image: quay.io/kubernetes-service-catalog/apiserver:v0.0.7 + image: quay.io/kubernetes-service-catalog/apiserver:v0.0.9 # imagePullPolicy for the apiserver; valid values are "IfNotPresent", # "Never", and "Always" imagePullPolicy: Always @@ -49,9 +49,14 @@ apiserver: # and authorization can be useful for quickly getting the walkthrough up and running, # but is not suitable for production. enabled: false + audit: + # If true, enables the use of audit features via this chart. + activated: false + # If specified, audit log goes to specified path. + logPath: "/tmp/service-catalog-apiserver-audit.log" controllerManager: # controller-manager image to use - image: quay.io/kubernetes-service-catalog/controller-manager:v0.0.7 + image: quay.io/kubernetes-service-catalog/controller-manager:v0.0.9 # imagePullPolicy for the controller-manager; valid values are # "IfNotPresent", "Never", and "Always" imagePullPolicy: Always @@ -64,4 +69,13 @@ controllerManager: # Whether or not the controller supports a --broker-relist-interval flag. If this is # set to true, brokerRelistInterval will be used as the value for that flag brokerRelistIntervalActivated: true + # enables profiling via web interface host:port/debug/pprof/ + profiling: + # Disable profiling via web interface host:port/debug/pprof/ + disabled: false + # Enables lock contention profiling, if profiling is enabled. + contentionProfiling: false + leaderElectionNamespace: + # Whether the controller has option to set leader election namespace. + activated: false useAggregator: false diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/ups-broker/values.yaml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/ups-broker/values.yaml index ff7f0568b39f..84ae88493e01 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/ups-broker/values.yaml +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/charts/ups-broker/values.yaml @@ -1,5 +1,5 @@ # Default values for User-Provided Service Broker # Image to use -image: quay.io/kubernetes-service-catalog/user-broker:v0.0.7 +image: quay.io/kubernetes-service-catalog/user-broker:v0.0.9 # ImagePullPolicy; valid values are "IfNotPresent", "Never", and "Always" imagePullPolicy: Always diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/options.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/options.go index 0bc79c5bc3a9..65e2ee67e1cb 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/options.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/options.go @@ -39,6 +39,8 @@ type ServiceCatalogServerOptions struct { AuthorizationOptions *genericserveroptions.DelegatingAuthorizationOptions // InsecureOptions are options for serving insecurely. InsecureServingOptions *genericserveroptions.ServingOptions + // audit options for api server + AuditOptions *genericserveroptions.AuditLogOptions // EtcdOptions are options for serving with etcd as the backing store EtcdOptions *EtcdOptions // TPROptions are options for serving with TPR as the backing store @@ -72,6 +74,7 @@ func (s *ServiceCatalogServerOptions) addFlags(flags *pflag.FlagSet) { s.InsecureServingOptions.AddFlags(flags) s.EtcdOptions.addFlags(flags) s.TPROptions.addFlags(flags) + s.AuditOptions.AddFlags(flags) } // StorageType returns the storage type configured on s, or a non-nil error if s holds an diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/server.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/server.go index 85958281893c..e7da34bb9907 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/server.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/server.go @@ -70,6 +70,7 @@ func NewCommandServer( AuthenticationOptions: genericserveroptions.NewDelegatingAuthenticationOptions(), AuthorizationOptions: genericserveroptions.NewDelegatingAuthorizationOptions(), InsecureServingOptions: genericserveroptions.NewInsecureServingOptions(), + AuditOptions: genericserveroptions.NewAuditLogOptions(), EtcdOptions: NewEtcdOptions(), TPROptions: NewTPROptions(), StopCh: stopCh, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/util.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/util.go index ca0d58ae13d4..0e0d86375891 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/util.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server/util.go @@ -86,8 +86,10 @@ func buildGenericConfig(s *ServiceCatalogServerOptions) (*genericapiserver.Confi glog.Infof("Authentication and authorization disabled for testing purposes") } - // TODO: add support for audit log options - // see https://github.com/kubernetes-incubator/service-catalog/issues/678 + if err := s.AuditOptions.ApplyTo(genericConfig); err != nil { + return nil, nil, err + } + // TODO: add support for OpenAPI config // see https://github.com/kubernetes-incubator/service-catalog/issues/721 genericConfig.SwaggerConfig = genericapiserver.DefaultSwaggerConfig() diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/controller_manager.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/controller_manager.go index 6a60d30256e3..97c7cad8159c 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/controller_manager.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/controller_manager.go @@ -21,7 +21,9 @@ import ( "fmt" "net" "net/http" + "net/http/pprof" "os" + goruntime "runtime" "strconv" "time" @@ -149,6 +151,15 @@ func Run(controllerManagerOptions *options.ControllerManagerServer) error { healthz.InstallHandler(mux) configz.InstallHandler(mux) + if controllerManagerOptions.EnableProfiling { + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + if controllerManagerOptions.EnableContentionProfiling { + goruntime.SetBlockProfileRate(1) + } + } server := &http.Server{ Addr: net.JoinHostPort(controllerManagerOptions.Address, strconv.Itoa(int(controllerManagerOptions.Port))), Handler: mux, @@ -197,10 +208,12 @@ func Run(controllerManagerOptions *options.ControllerManagerServer) error { return err } + glog.V(5).Infof("Using namespace %v for leader election lock", controllerManagerOptions.LeaderElectionNamespace) + // Lock required for leader election rl := resourcelock.EndpointsLock{ EndpointsMeta: metav1.ObjectMeta{ - Namespace: "kube-system", + Namespace: controllerManagerOptions.LeaderElectionNamespace, Name: "service-catalog-controller-manager", }, Client: leaderElectionClient, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/options/options.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/options/options.go index dcb782e4c0c0..2977f67fe5e2 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/options/options.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/cmd/controller-manager/app/options/options.go @@ -36,15 +36,18 @@ type ControllerManagerServer struct { componentconfig.ControllerManagerConfiguration } -const defaultResyncInterval = 5 * time.Minute -const defaultBrokerRelistInterval = 24 * time.Hour -const defaultContentType = "application/json" -const defaultBindAddress = "0.0.0.0" -const defaultPort = 10000 -const defaultK8sKubeconfigPath = "./kubeconfig" -const defaultServiceCatalogKubeconfigPath = "./service-catalog-kubeconfig" -const defaultOSBAPIContextProfile = true -const defaultConcurrentSyncs = 5 +const ( + defaultResyncInterval = 5 * time.Minute + defaultBrokerRelistInterval = 24 * time.Hour + defaultContentType = "application/json" + defaultBindAddress = "0.0.0.0" + defaultPort = 10000 + defaultK8sKubeconfigPath = "./kubeconfig" + defaultServiceCatalogKubeconfigPath = "./service-catalog-kubeconfig" + defaultOSBAPIContextProfile = true + defaultConcurrentSyncs = 5 + defaultLeaderElectionNamespace = "kube-system" +) // NewControllerManagerServer creates a new ControllerManagerServer with a // default config. @@ -61,6 +64,9 @@ func NewControllerManagerServer() *ControllerManagerServer { OSBAPIContextProfile: defaultOSBAPIContextProfile, ConcurrentSyncs: defaultConcurrentSyncs, LeaderElection: leaderelection.DefaultLeaderElectionConfiguration(), + LeaderElectionNamespace: defaultLeaderElectionNamespace, + EnableProfiling: true, + EnableContentionProfiling: false, }, } s.LeaderElection.LeaderElect = true @@ -79,5 +85,8 @@ func (s *ControllerManagerServer) AddFlags(fs *pflag.FlagSet) { fs.DurationVar(&s.ResyncInterval, "resync-interval", s.ResyncInterval, "The interval on which the controller will resync its informers") fs.DurationVar(&s.BrokerRelistInterval, "broker-relist-interval", s.BrokerRelistInterval, "The interval on which a broker's catalog is relisted after the broker becomes ready") fs.BoolVar(&s.OSBAPIContextProfile, "enable-osb-api-context-profile", s.OSBAPIContextProfile, "Whether or not to send the proposed optional OpenServiceBroker API Context Profile field") + fs.BoolVar(&s.EnableProfiling, "profiling", s.EnableProfiling, "Enable profiling via web interface host:port/debug/pprof/") + fs.BoolVar(&s.EnableContentionProfiling, "contention-profiling", s.EnableContentionProfiling, "Enable lock contention profiling, if profiling is enabled") leaderelection.BindFlags(&s.LeaderElection, fs) + fs.StringVar(&s.LeaderElectionNamespace, "leader-election-namespace", s.LeaderElectionNamespace, "Namespace to use for leader election lock") } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/contrib/examples/apiserver/broker.yaml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/contrib/examples/apiserver/broker.yaml index 0ffcedab8d47..4eb55adca3c5 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/contrib/examples/apiserver/broker.yaml +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/contrib/examples/apiserver/broker.yaml @@ -7,6 +7,7 @@ spec: # put the basic auth for the broker in a secret, and reference the secret here. # service-catalog will use the contents of the secret. The secret should have "username" # and "password" keys - authSecret: - namespace: some-namespace - name: secret-name + authInfo: + basicAuthSecret: + namespace: some-namespace + name: secret-name diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/contrib/examples/walkthrough/ups-binding-pp.yaml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/contrib/examples/walkthrough/ups-binding-pp.yaml new file mode 100644 index 000000000000..fa38f5ab42a1 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/contrib/examples/walkthrough/ups-binding-pp.yaml @@ -0,0 +1,14 @@ +apiVersion: servicecatalog.k8s.io/v1alpha1 +kind: Binding +metadata: + name: ups-binding + namespace: test-ns +spec: + instanceRef: + name: ups-instance + secretName: my-secret + alphaPodPresetTemplate: + name: my-pod-preset + selector: + matchLabels: + app: my-app diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/docs/auth.md b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/docs/auth.md index c42e5037a792..05f394366f39 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/docs/auth.md +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/docs/auth.md @@ -136,13 +136,19 @@ use the following commands: ```shell export SERVICE_NAME= -export ALT_NAMES=".,..svc" +export ALT_NAMES='".","..svc"' echo '{"CN":"'${SERVICE_NAME}'","hosts":['${ALT_NAMES}'],"key":{"algo":"rsa","size":2048}}' | cfssl gencert -ca=server-ca.crt -ca-key=server-ca.key -config=server-ca-config.json - | cfssljson -bare apiserver ``` `` should be the name of the Service for service catalog API server (e.g. `-` when using Helm). +This will create a pair of files named `apiserver-key.pem` and +`apiserver.pem`. These are the private key and public certificate, +respectively. The private key and certificate are commonly referred to +with `.key ` and `.crt` extensions, respectively: `apiserver.key` and +`apiserver.crt`. + To base64 encode these files for passing to the Helm charts, run `base64 --wrap=0 `. The resulting output may be passed to the Helm charts for the `apiserver.tls.*` series of options. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.lock b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.lock index 766c0f0d5740..9c8d2a400f8c 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.lock +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.lock @@ -1,5 +1,5 @@ -hash: 1d3632dc4b800ded5ca641abeeed846aa103e17a69fd80c023c1c66da0c30c4d -updated: 2017-05-05T11:55:59.493581821-07:00 +hash: 62c3838a43f667530907c83fc266d0aaa72b42a27b1fa8cf834844f61c6c5eb8 +updated: 2017-06-09T09:44:21.859689962-07:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -8,6 +8,8 @@ imports: subpackages: - compute/metadata - internal +- name: code.cloudfoundry.org/lager + version: 62951a8009ab331bb21dc418074fa54e66eb9b6a - name: github.com/beorn7/perks version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 subpackages: @@ -97,7 +99,7 @@ imports: - name: github.com/gorilla/handlers version: 13d73096a474cac93275c679c7b8a2dc17ddba82 - name: github.com/gorilla/mux - version: 599cba5e7b6137d46ddf58fb1765f5d928e69604 + version: bcd8bc72b08df0f70df986b97f95590779502d31 - name: github.com/grpc-ecosystem/grpc-gateway version: f52d055dc48aec25854ed7d31862f78913cf17d1 subpackages: @@ -164,6 +166,10 @@ imports: - types - name: github.com/pborman/uuid version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 +- name: github.com/pivotal-cf/brokerapi + version: 6d25b9398d9f05880ca8f480134a88c8d2df69bc + subpackages: + - auth - name: github.com/pkg/errors version: a22138067af1c4942683050411a841ade67fe1eb - name: github.com/prometheus/client_golang @@ -549,9 +555,70 @@ imports: version: 9497139cb62015905ba5b3d11836f2b0c117ff80 subpackages: - pkg/api + - pkg/api/install - pkg/api/v1 + - pkg/apis/apps + - pkg/apis/apps/install + - pkg/apis/apps/v1beta1 + - pkg/apis/authentication + - pkg/apis/authentication/install + - pkg/apis/authentication/v1 + - pkg/apis/authentication/v1beta1 + - pkg/apis/authorization + - pkg/apis/authorization/install + - pkg/apis/authorization/v1 + - pkg/apis/authorization/v1beta1 + - pkg/apis/autoscaling + - pkg/apis/autoscaling/install + - pkg/apis/autoscaling/v1 + - pkg/apis/autoscaling/v2alpha1 + - pkg/apis/batch + - pkg/apis/batch/install + - pkg/apis/batch/v1 + - pkg/apis/batch/v2alpha1 + - pkg/apis/certificates + - pkg/apis/certificates/install + - pkg/apis/certificates/v1beta1 - pkg/apis/componentconfig - pkg/apis/extensions + - pkg/apis/extensions/install + - pkg/apis/extensions/v1beta1 + - pkg/apis/policy + - pkg/apis/policy/install + - pkg/apis/policy/v1beta1 + - pkg/apis/rbac + - pkg/apis/rbac/install + - pkg/apis/rbac/v1alpha1 + - pkg/apis/rbac/v1beta1 + - pkg/apis/settings + - pkg/apis/settings/install + - pkg/apis/settings/v1alpha1 + - pkg/apis/storage + - pkg/apis/storage/install + - pkg/apis/storage/v1 + - pkg/apis/storage/v1beta1 + - pkg/client/clientset_generated/clientset + - pkg/client/clientset_generated/clientset/scheme + - pkg/client/clientset_generated/clientset/typed/apps/v1beta1 + - pkg/client/clientset_generated/clientset/typed/authentication/v1 + - pkg/client/clientset_generated/clientset/typed/authentication/v1beta1 + - pkg/client/clientset_generated/clientset/typed/authorization/v1 + - pkg/client/clientset_generated/clientset/typed/authorization/v1beta1 + - pkg/client/clientset_generated/clientset/typed/autoscaling/v1 + - pkg/client/clientset_generated/clientset/typed/autoscaling/v2alpha1 + - pkg/client/clientset_generated/clientset/typed/batch/v1 + - pkg/client/clientset_generated/clientset/typed/batch/v2alpha1 + - pkg/client/clientset_generated/clientset/typed/certificates/v1beta1 + - pkg/client/clientset_generated/clientset/typed/core/v1 + - pkg/client/clientset_generated/clientset/typed/extensions/v1beta1 + - pkg/client/clientset_generated/clientset/typed/policy/v1beta1 + - pkg/client/clientset_generated/clientset/typed/rbac/v1alpha1 + - pkg/client/clientset_generated/clientset/typed/rbac/v1beta1 + - pkg/client/clientset_generated/clientset/typed/settings/v1alpha1 + - pkg/client/clientset_generated/clientset/typed/storage/v1 + - pkg/client/clientset_generated/clientset/typed/storage/v1beta1 + - pkg/client/leaderelection + - pkg/client/leaderelection/resourcelock - pkg/util - pkg/util/configz - pkg/util/interrupt diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.yaml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.yaml index 602ba2cc60ba..5f61dc06af0b 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.yaml +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/glide.yaml @@ -65,3 +65,5 @@ import: version: v1.1.0 - package: github.com/onsi/ginkgo version: v1.3.1 +- package: github.com/pivotal-cf/brokerapi +- package: code.cloudfoundry.org/lager diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/deletion.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/deletion.go new file mode 100644 index 000000000000..c69145cf915e --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/deletion.go @@ -0,0 +1,73 @@ +/* +Copyright 2017 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 meta + +import ( + "errors" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var ( + // ErrNoDeletionTimestamp is the error returned by GetDeletionTimestamp when there is no + // deletion timestamp set on the object + ErrNoDeletionTimestamp = errors.New("no deletion timestamp set") +) + +// DeletionTimestampExists returns true if a deletion timestamp exists on obj, or a non-nil +// error if that couldn't be reliably determined +func DeletionTimestampExists(obj runtime.Object) (bool, error) { + _, err := GetDeletionTimestamp(obj) + if err == ErrNoDeletionTimestamp { + // if GetDeletionTimestamp reported that no deletion timestamp exists, return false + // and no error + return false, nil + } + if err != nil { + // otherwise, if GetDeletionTimestamp returned an unknown error, return the error + return false, err + } + return true, nil +} + +// GetDeletionTimestamp returns the deletion timestamp on obj, or a non-nil error if there was +// an error getting it or it isn't set. Returns ErrNoDeletionTimestamp if there was none set +func GetDeletionTimestamp(obj runtime.Object) (*metav1.Time, error) { + accessor, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + t := accessor.GetDeletionTimestamp() + if t == nil { + return nil, ErrNoDeletionTimestamp + } + return t, nil +} + +// SetDeletionTimestamp sets the deletion timestamp on obj to t +func SetDeletionTimestamp(obj runtime.Object, t time.Time) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + metaTime := metav1.NewTime(t) + accessor.SetDeletionTimestamp(&metaTime) + return nil +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/deletion_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/deletion_test.go new file mode 100644 index 000000000000..adc8aa61f867 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/deletion_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2017 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 meta + +import ( + "testing" + "time" + + sc "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDeletionTimestampExists(t *testing.T) { + obj := &sc.Instance{ + ObjectMeta: metav1.ObjectMeta{}, + } + exists, err := DeletionTimestampExists(obj) + if err != nil { + t.Fatal(err) + } + if exists { + t.Fatalf("deletion timestamp reported as exists when it didn't") + } + tme := metav1.NewTime(time.Now()) + obj.DeletionTimestamp = &tme + exists, err = DeletionTimestampExists(obj) + if err != nil { + t.Fatal(err) + } + if !exists { + t.Fatal("deletion timestamp reported as missing when it isn't") + } +} + +func TestRoundTripDeletionTimestamp(t *testing.T) { + t1 := metav1.NewTime(time.Now()) + t2 := metav1.NewTime(time.Now().Add(1 * time.Hour)) + obj := &sc.Instance{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &t1, + }, + } + t1Ret, err := GetDeletionTimestamp(obj) + if err != nil { + t.Fatalf("error getting 1st deletion timestamp (%s)", err) + } + if !t1.Equal(*t1Ret) { + t.Fatalf("expected deletion timestamp %s, got %s", t1, *t1Ret) + } + if err := SetDeletionTimestamp(obj, t2.Time); err != nil { + t.Fatalf("error setting deletion timestamp (%s)", err) + } + t2Ret, err := GetDeletionTimestamp(obj) + if err != nil { + t.Fatalf("error getting 2nd deletion timestamp (%s)", err) + } + if !t2.Equal(*t2Ret) { + t.Fatalf("expected deletion timestamp %s, got %s", t2, *t2Ret) + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/finalizers.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/finalizers.go new file mode 100644 index 000000000000..3a5f05c10909 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/finalizers.go @@ -0,0 +1,58 @@ +/* +Copyright 2017 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 meta + +import ( + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" +) + +// GetFinalizers gets the list of finalizers on obj +func GetFinalizers(obj runtime.Object) ([]string, error) { + accessor, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + return accessor.GetFinalizers(), nil +} + +// AddFinalizer adds value to the list of finalizers on obj +func AddFinalizer(obj runtime.Object, value string) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + finalizers := sets.NewString(accessor.GetFinalizers()...) + finalizers.Insert(value) + accessor.SetFinalizers(finalizers.List()) + return nil +} + +// RemoveFinalizer removes the given value from the list of finalizers in obj, then returns a new list +// of finalizers after value has been removed. +func RemoveFinalizer(obj runtime.Object, value string) ([]string, error) { + accessor, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + finalizers := sets.NewString(accessor.GetFinalizers()...) + finalizers.Delete(value) + newFinalizers := finalizers.List() + accessor.SetFinalizers(newFinalizers) + return newFinalizers, nil +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/finalizers_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/finalizers_test.go new file mode 100644 index 000000000000..e503f6866820 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/finalizers_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2017 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 meta + +import ( + "testing" + + sc "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + testFinalizer = "testfinalizer" +) + +func TestGetFinalizers(t *testing.T) { + obj := &sc.Instance{ + ObjectMeta: metav1.ObjectMeta{Finalizers: []string{testFinalizer}}, + } + finalizers, err := GetFinalizers(obj) + if err != nil { + t.Fatal(err) + } + if len(finalizers) != 1 { + t.Fatalf("expected 1 finalizer, got %d", len(finalizers)) + } + if finalizers[0] != testFinalizer { + t.Fatalf("expected finalizer %s, got %s", testFinalizer, finalizers[0]) + } +} + +func TestAddFinalizer(t *testing.T) { + obj := &sc.Instance{ + ObjectMeta: metav1.ObjectMeta{}, + } + if err := AddFinalizer(obj, testFinalizer); err != nil { + t.Fatal(err) + } + if len(obj.Finalizers) != 1 { + t.Fatalf("expected 1 finalizer, got %d", len(obj.Finalizers)) + } + if obj.Finalizers[0] != testFinalizer { + t.Fatalf("expected finalizer %s, got %s", testFinalizer, obj.Finalizers[0]) + } +} + +func TestRemoveFinalizer(t *testing.T) { + obj := &sc.Instance{ + ObjectMeta: metav1.ObjectMeta{Finalizers: []string{testFinalizer}}, + } + newFinalizers, err := RemoveFinalizer(obj, testFinalizer+"-noexist") + if err != nil { + t.Fatalf("error removing non-existent finalizer (%s)", err) + } + if len(newFinalizers) != 1 { + t.Fatalf("number of returned finalizers wasn't 1") + } + if len(obj.Finalizers) != 1 { + t.Fatalf("finalizer was removed when it shouldn't have been") + } + if obj.Finalizers[0] != testFinalizer { + t.Fatalf("expected finalizer %s, got %s", testFinalizer, obj.Finalizers[0]) + } + newFinalizers, err = RemoveFinalizer(obj, testFinalizer) + if err != nil { + t.Fatalf("error removing existent finalizer (%s)", err) + } + if len(newFinalizers) != 0 { + t.Fatalf("number of returned finalizers wasn't 0") + } + if len(obj.Finalizers) != 0 { + t.Fatalf("expected no finalizers, got %d", len(obj.Finalizers)) + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/metadata_access.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/init.go similarity index 79% rename from cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/metadata_access.go rename to cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/init.go index e1510c91b921..109fbd63accb 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/metadata_access.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/init.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tpr +package meta import ( "k8s.io/apimachinery/pkg/api/meta" @@ -31,9 +31,3 @@ var ( func GetAccessor() meta.MetadataAccessor { return accessor } - -// GetNamespace returns the namespace for the given object, if there is one. If not, returns -// the empty string and a non-nil error -func GetNamespace(obj runtime.Object) (string, error) { - return selfLinker.Namespace(obj) -} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/init_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/init_test.go new file mode 100644 index 000000000000..cdeb4f089dea --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/init_test.go @@ -0,0 +1,27 @@ +/* +Copyright 2017 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 meta + +import ( + "testing" +) + +func TestGetAccessor(t *testing.T) { + if GetAccessor() != accessor { + t.Fatalf("GetAccessor didn't return the pre-initialized accessor") + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/namespace.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/namespace.go new file mode 100644 index 000000000000..ed4b553304ee --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/namespace.go @@ -0,0 +1,27 @@ +/* +Copyright 2017 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 meta + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// GetNamespace returns the namespace for the given object, if there is one. If not, returns +// the empty string and a non-nil error +func GetNamespace(obj runtime.Object) (string, error) { + return selfLinker.Namespace(obj) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/namespace_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/namespace_test.go new file mode 100644 index 000000000000..c683d61d7860 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/api/meta/namespace_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2017 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 meta + +import ( + "testing" + + sc "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetNamespace(t *testing.T) { + const namespace = "testns" + obj := &sc.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + } + ns, err := GetNamespace(obj) + if err != nil { + t.Fatalf("error getting namespace (%s)", err) + } + if ns != namespace { + t.Fatalf("actual namespace (%s) wasn't expected (%s)", ns, namespace) + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/componentconfig/types.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/componentconfig/types.go index b17b8e8c7b25..e89173764691 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/componentconfig/types.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/componentconfig/types.go @@ -74,4 +74,14 @@ type ControllerManagerConfiguration struct { // leaderElection defines the configuration of leader election client. LeaderElection componentconfig.LeaderElectionConfiguration + + // LeaderElectionNamespace is the namespace to use for the leader election + // lock. + LeaderElectionNamespace string + + // enableProfiling enables profiling via web interface host:port/debug/pprof/ + EnableProfiling bool + + // enableContentionProfiling enables lock contention profiling, if enableProfiling is true. + EnableContentionProfiling bool } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/testing/fuzzer.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/testing/fuzzer.go index f75511c3441d..72246382994d 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/testing/fuzzer.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/testing/fuzzer.go @@ -168,7 +168,7 @@ func FuzzerFor(t *testing.T, version schema.GroupVersion, src rand.Source) *fuzz c.Fuzz(obj) // Find a codec for converting the object to raw bytes. This is necessary for the - // api version and kind to be correctly set be serialization. + // api version and kind to be correctly set by serialization. var codec runtime.Codec switch obj.(type) { case *api.Pod: @@ -229,6 +229,9 @@ func FuzzerFor(t *testing.T, version schema.GroupVersion, src rand.Source) *fuzz return } sp.ExternalMetadata = metadata + sp.AlphaBindingCreateParameterSchema = metadata + sp.AlphaInstanceCreateParameterSchema = metadata + sp.AlphaInstanceUpdateParameterSchema = metadata }, ) return f diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/types.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/types.go index bd5379ba3b20..ff474fe3b87e 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/types.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/types.go @@ -48,9 +48,21 @@ type BrokerSpec struct { // URL is the address used to communicate with the Broker. URL string - // AuthSecret is a reference to a Secret containing auth information the - // catalog should use to authenticate to this Broker. - AuthSecret *v1.ObjectReference + // AuthInfo contains the data that the service catalog should use to authenticate + // with the Broker. + AuthInfo *BrokerAuthInfo +} + +// BrokerAuthInfo is a union type that contains information on one of the authentication methods +// the the service catalog and brokers may support, according to the OpenServiceBroker API +// specification (https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md). +// +// Note that we currently restrict a single broker to have only one of these fields +// set on it. +type BrokerAuthInfo struct { + // BasicAuthSecret is a reference to a Secret containing auth information the + // catalog should use to authenticate to this Broker using basic auth. + BasicAuthSecret *v1.ObjectReference } // BrokerStatus represents the current status of a Broker. @@ -62,14 +74,18 @@ type BrokerStatus struct { type BrokerCondition struct { // Type of the condition, currently ('Ready'). Type BrokerConditionType + // Status of the condition, one of ('True', 'False', 'Unknown'). Status ConditionStatus + // LastTransitionTime is the timestamp corresponding to the last status // change of this condition. LastTransitionTime metav1.Time + // Reason is a brief machine readable explanation for the condition's last // transition. Reason string + // Message is a human readable description of the details of the last // transition, complementing reason. Message string @@ -95,8 +111,10 @@ type ConditionStatus string const ( // ConditionTrue represents the fact that a given condition is true ConditionTrue ConditionStatus = "True" + // ConditionFalse represents the fact that a given condition is false ConditionFalse ConditionStatus = "False" + // ConditionUnknown represents the fact that a given condition is unknown ConditionUnknown ConditionStatus = "Unknown" ) @@ -193,6 +211,28 @@ type ServicePlan struct { // user-facing content and display instructions. This field may contain // platform-specific conventional values. ExternalMetadata *runtime.RawExtension + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaInstanceCreateParameterSchema is the schema for the parameters + // that may be supplied when provisioning a new Instance on this plan. + AlphaInstanceCreateParameterSchema *runtime.RawExtension + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaInstanceUpdateParameterSchema is the schema for the parameters + // that may be updated once an Instance has been provisioned on this plan. + // This field only has meaning if the ServiceClass is PlanUpdatable. + AlphaInstanceUpdateParameterSchema *runtime.RawExtension + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaBindingCreateParameterSchema is the schema for the parameters that + // may be supplied binding to an Instance on this plan. + AlphaBindingCreateParameterSchema *runtime.RawExtension } // InstanceList is a list of instances. @@ -264,14 +304,18 @@ type InstanceStatus struct { type InstanceCondition struct { // Type of the condition, currently ('Ready'). Type InstanceConditionType + // Status of the condition, one of ('True', 'False', 'Unknown'). Status ConditionStatus + // LastTransitionTime is the timestamp corresponding to the last status // change of this condition. LastTransitionTime metav1.Time + // Reason is a brief machine readable explanation for the condition's last // transition. Reason string + // Message is a human readable description of the details of the last // transition, complementing reason. Message string @@ -325,6 +369,28 @@ type BindingSpec struct { // // Immutable. ExternalID string + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaPodPresetTemplate describes how a PodPreset should be created once + // the Binding has been made. If supplied, a PodPreset will be created + // using information in this field once the Binding has been made in the + // Broker. The PodPreset will use the EnvFrom feature to expose the keys + // from the Secret (specified by SecretName) that holds the Binding + // information into Pods. + // + // In the future, we will provide a higher degree of control over the PodPreset. + AlphaPodPresetTemplate *AlphaPodPresetTemplate +} + +// AlphaPodPresetTemplate represents how a PodPreset should be created for a +// Binding. +type AlphaPodPresetTemplate struct { + // Name is the name of the PodPreset to create. + Name string + // Selector is the LabelSelector of the PodPreset to create. + Selector metav1.LabelSelector } // BindingStatus represents the current status of a Binding. @@ -340,14 +406,18 @@ type BindingStatus struct { type BindingCondition struct { // Type of the condition, currently ('Ready'). Type BindingConditionType + // Status of the condition, one of ('True', 'False', 'Unknown'). Status ConditionStatus + // LastTransitionTime is the timestamp corresponding to the last status // change of this condition. LastTransitionTime metav1.Time + // Reason is a brief machine readable explanation for the condition's last // transition. Reason string + // Message is a human readable description of the details of the last // transition, complementing reason. Message string @@ -360,3 +430,8 @@ const ( // BindingConditionReady represents a BindingCondition is in ready state. BindingConditionReady BindingConditionType = "Ready" ) + +// These are internal finalizer values to service catalog, must be qualified name. +const ( + FinalizerServiceCatalog string = "kubernetes-incubator/service-catalog" +) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.generated.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.generated.go index 5de604c5e688..0a3dae08a6ac 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.generated.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.generated.go @@ -835,7 +835,7 @@ func (x *BrokerSpec) CodecEncodeSelf(e *codec1978.Encoder) { var yyq2 [2]bool _, _, _ = yysep2, yyq2, yy2arr2 const yyr2 bool = false - yyq2[1] = x.AuthSecret != nil + yyq2[1] = x.AuthInfo != nil var yynn2 int if yyr2 || yy2arr2 { r.EncodeArrayStart(2) @@ -871,10 +871,10 @@ func (x *BrokerSpec) CodecEncodeSelf(e *codec1978.Encoder) { if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayElem1234) if yyq2[1] { - if x.AuthSecret == nil { + if x.AuthInfo == nil { r.EncodeNil() } else { - x.AuthSecret.CodecEncodeSelf(e) + x.AuthInfo.CodecEncodeSelf(e) } } else { r.EncodeNil() @@ -882,12 +882,12 @@ func (x *BrokerSpec) CodecEncodeSelf(e *codec1978.Encoder) { } else { if yyq2[1] { z.EncSendContainerState(codecSelfer_containerMapKey1234) - r.EncodeString(codecSelferC_UTF81234, string("authSecret")) + r.EncodeString(codecSelferC_UTF81234, string("authInfo")) z.EncSendContainerState(codecSelfer_containerMapValue1234) - if x.AuthSecret == nil { + if x.AuthInfo == nil { r.EncodeNil() } else { - x.AuthSecret.CodecEncodeSelf(e) + x.AuthInfo.CodecEncodeSelf(e) } } } @@ -964,16 +964,16 @@ func (x *BrokerSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { *((*string)(yyv4)) = r.DecodeString() } } - case "authSecret": + case "authInfo": if r.TryDecodeAsNil() { - if x.AuthSecret != nil { - x.AuthSecret = nil + if x.AuthInfo != nil { + x.AuthInfo = nil } } else { - if x.AuthSecret == nil { - x.AuthSecret = new(pkg3_v1.ObjectReference) + if x.AuthInfo == nil { + x.AuthInfo = new(BrokerAuthInfo) } - x.AuthSecret.CodecDecodeSelf(d) + x.AuthInfo.CodecDecodeSelf(d) } default: z.DecStructFieldNotFound(-1, yys3) @@ -1023,14 +1023,14 @@ func (x *BrokerSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { } z.DecSendContainerState(codecSelfer_containerArrayElem1234) if r.TryDecodeAsNil() { - if x.AuthSecret != nil { - x.AuthSecret = nil + if x.AuthInfo != nil { + x.AuthInfo = nil } } else { - if x.AuthSecret == nil { - x.AuthSecret = new(pkg3_v1.ObjectReference) + if x.AuthInfo == nil { + x.AuthInfo = new(BrokerAuthInfo) } - x.AuthSecret.CodecDecodeSelf(d) + x.AuthInfo.CodecDecodeSelf(d) } for { yyj7++ @@ -1048,6 +1048,183 @@ func (x *BrokerSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) } +func (x *BrokerAuthInfo) CodecEncodeSelf(e *codec1978.Encoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperEncoder(e) + _, _, _ = h, z, r + if x == nil { + r.EncodeNil() + } else { + yym1 := z.EncBinary() + _ = yym1 + if false { + } else if z.HasExtensions() && z.EncExt(x) { + } else { + yysep2 := !z.EncBinary() + yy2arr2 := z.EncBasicHandle().StructToArray + var yyq2 [1]bool + _, _, _ = yysep2, yyq2, yy2arr2 + const yyr2 bool = false + yyq2[0] = x.BasicAuthSecret != nil + var yynn2 int + if yyr2 || yy2arr2 { + r.EncodeArrayStart(1) + } else { + yynn2 = 0 + for _, b := range yyq2 { + if b { + yynn2++ + } + } + r.EncodeMapStart(yynn2) + yynn2 = 0 + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + if yyq2[0] { + if x.BasicAuthSecret == nil { + r.EncodeNil() + } else { + x.BasicAuthSecret.CodecEncodeSelf(e) + } + } else { + r.EncodeNil() + } + } else { + if yyq2[0] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("basicAuthSecret")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.BasicAuthSecret == nil { + r.EncodeNil() + } else { + x.BasicAuthSecret.CodecEncodeSelf(e) + } + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayEnd1234) + } else { + z.EncSendContainerState(codecSelfer_containerMapEnd1234) + } + } + } +} + +func (x *BrokerAuthInfo) CodecDecodeSelf(d *codec1978.Decoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperDecoder(d) + _, _, _ = h, z, r + yym1 := z.DecBinary() + _ = yym1 + if false { + } else if z.HasExtensions() && z.DecExt(x) { + } else { + yyct2 := r.ContainerType() + if yyct2 == codecSelferValueTypeMap1234 { + yyl2 := r.ReadMapStart() + if yyl2 == 0 { + z.DecSendContainerState(codecSelfer_containerMapEnd1234) + } else { + x.codecDecodeSelfFromMap(yyl2, d) + } + } else if yyct2 == codecSelferValueTypeArray1234 { + yyl2 := r.ReadArrayStart() + if yyl2 == 0 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + } else { + x.codecDecodeSelfFromArray(yyl2, d) + } + } else { + panic(codecSelferOnlyMapOrArrayEncodeToStructErr1234) + } + } +} + +func (x *BrokerAuthInfo) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperDecoder(d) + _, _, _ = h, z, r + var yys3Slc = z.DecScratchBuffer() // default slice to decode into + _ = yys3Slc + var yyhl3 bool = l >= 0 + for yyj3 := 0; ; yyj3++ { + if yyhl3 { + if yyj3 >= l { + break + } + } else { + if r.CheckBreak() { + break + } + } + z.DecSendContainerState(codecSelfer_containerMapKey1234) + yys3Slc = r.DecodeBytes(yys3Slc, true, true) + yys3 := string(yys3Slc) + z.DecSendContainerState(codecSelfer_containerMapValue1234) + switch yys3 { + case "basicAuthSecret": + if r.TryDecodeAsNil() { + if x.BasicAuthSecret != nil { + x.BasicAuthSecret = nil + } + } else { + if x.BasicAuthSecret == nil { + x.BasicAuthSecret = new(pkg3_v1.ObjectReference) + } + x.BasicAuthSecret.CodecDecodeSelf(d) + } + default: + z.DecStructFieldNotFound(-1, yys3) + } // end switch yys3 + } // end for yyj3 + z.DecSendContainerState(codecSelfer_containerMapEnd1234) +} + +func (x *BrokerAuthInfo) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperDecoder(d) + _, _, _ = h, z, r + var yyj5 int + var yyb5 bool + var yyhl5 bool = l >= 0 + yyj5++ + if yyhl5 { + yyb5 = yyj5 > l + } else { + yyb5 = r.CheckBreak() + } + if yyb5 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.BasicAuthSecret != nil { + x.BasicAuthSecret = nil + } + } else { + if x.BasicAuthSecret == nil { + x.BasicAuthSecret = new(pkg3_v1.ObjectReference) + } + x.BasicAuthSecret.CodecDecodeSelf(d) + } + for { + yyj5++ + if yyhl5 { + yyb5 = yyj5 > l + } else { + yyb5 = r.CheckBreak() + } + if yyb5 { + break + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + z.DecStructFieldNotFound(yyj5-1, "") + } + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) +} + func (x *BrokerStatus) CodecEncodeSelf(e *codec1978.Encoder) { var h codecSelfer1234 z, r := codec1978.GenHelperEncoder(e) @@ -2882,13 +3059,16 @@ func (x *ServicePlan) CodecEncodeSelf(e *codec1978.Encoder) { } else { yysep2 := !z.EncBinary() yy2arr2 := z.EncBasicHandle().StructToArray - var yyq2 [6]bool + var yyq2 [9]bool _, _, _ = yysep2, yyq2, yy2arr2 const yyr2 bool = false yyq2[3] = x.Bindable != nil + yyq2[6] = x.AlphaInstanceCreateParameterSchema != nil + yyq2[7] = x.AlphaInstanceUpdateParameterSchema != nil + yyq2[8] = x.AlphaBindingCreateParameterSchema != nil var yynn2 int if yyr2 || yy2arr2 { - r.EncodeArrayStart(6) + r.EncodeArrayStart(9) } else { yynn2 = 5 for _, b := range yyq2 { @@ -3043,6 +3223,123 @@ func (x *ServicePlan) CodecEncodeSelf(e *codec1978.Encoder) { } } } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + if yyq2[6] { + if x.AlphaInstanceCreateParameterSchema == nil { + r.EncodeNil() + } else { + yym24 := z.EncBinary() + _ = yym24 + if false { + } else if z.HasExtensions() && z.EncExt(x.AlphaInstanceCreateParameterSchema) { + } else if !yym24 && z.IsJSONHandle() { + z.EncJSONMarshal(x.AlphaInstanceCreateParameterSchema) + } else { + z.EncFallback(x.AlphaInstanceCreateParameterSchema) + } + } + } else { + r.EncodeNil() + } + } else { + if yyq2[6] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("alphaInstanceCreateParameterSchema")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.AlphaInstanceCreateParameterSchema == nil { + r.EncodeNil() + } else { + yym25 := z.EncBinary() + _ = yym25 + if false { + } else if z.HasExtensions() && z.EncExt(x.AlphaInstanceCreateParameterSchema) { + } else if !yym25 && z.IsJSONHandle() { + z.EncJSONMarshal(x.AlphaInstanceCreateParameterSchema) + } else { + z.EncFallback(x.AlphaInstanceCreateParameterSchema) + } + } + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + if yyq2[7] { + if x.AlphaInstanceUpdateParameterSchema == nil { + r.EncodeNil() + } else { + yym27 := z.EncBinary() + _ = yym27 + if false { + } else if z.HasExtensions() && z.EncExt(x.AlphaInstanceUpdateParameterSchema) { + } else if !yym27 && z.IsJSONHandle() { + z.EncJSONMarshal(x.AlphaInstanceUpdateParameterSchema) + } else { + z.EncFallback(x.AlphaInstanceUpdateParameterSchema) + } + } + } else { + r.EncodeNil() + } + } else { + if yyq2[7] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("alphaInstanceUpdateParameterSchema")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.AlphaInstanceUpdateParameterSchema == nil { + r.EncodeNil() + } else { + yym28 := z.EncBinary() + _ = yym28 + if false { + } else if z.HasExtensions() && z.EncExt(x.AlphaInstanceUpdateParameterSchema) { + } else if !yym28 && z.IsJSONHandle() { + z.EncJSONMarshal(x.AlphaInstanceUpdateParameterSchema) + } else { + z.EncFallback(x.AlphaInstanceUpdateParameterSchema) + } + } + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + if yyq2[8] { + if x.AlphaBindingCreateParameterSchema == nil { + r.EncodeNil() + } else { + yym30 := z.EncBinary() + _ = yym30 + if false { + } else if z.HasExtensions() && z.EncExt(x.AlphaBindingCreateParameterSchema) { + } else if !yym30 && z.IsJSONHandle() { + z.EncJSONMarshal(x.AlphaBindingCreateParameterSchema) + } else { + z.EncFallback(x.AlphaBindingCreateParameterSchema) + } + } + } else { + r.EncodeNil() + } + } else { + if yyq2[8] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("alphaBindingCreateParameterSchema")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.AlphaBindingCreateParameterSchema == nil { + r.EncodeNil() + } else { + yym31 := z.EncBinary() + _ = yym31 + if false { + } else if z.HasExtensions() && z.EncExt(x.AlphaBindingCreateParameterSchema) { + } else if !yym31 && z.IsJSONHandle() { + z.EncJSONMarshal(x.AlphaBindingCreateParameterSchema) + } else { + z.EncFallback(x.AlphaBindingCreateParameterSchema) + } + } + } + } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayEnd1234) } else { @@ -3187,6 +3484,63 @@ func (x *ServicePlan) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { z.DecFallback(x.ExternalMetadata, false) } } + case "alphaInstanceCreateParameterSchema": + if r.TryDecodeAsNil() { + if x.AlphaInstanceCreateParameterSchema != nil { + x.AlphaInstanceCreateParameterSchema = nil + } + } else { + if x.AlphaInstanceCreateParameterSchema == nil { + x.AlphaInstanceCreateParameterSchema = new(pkg4_runtime.RawExtension) + } + yym17 := z.DecBinary() + _ = yym17 + if false { + } else if z.HasExtensions() && z.DecExt(x.AlphaInstanceCreateParameterSchema) { + } else if !yym17 && z.IsJSONHandle() { + z.DecJSONUnmarshal(x.AlphaInstanceCreateParameterSchema) + } else { + z.DecFallback(x.AlphaInstanceCreateParameterSchema, false) + } + } + case "alphaInstanceUpdateParameterSchema": + if r.TryDecodeAsNil() { + if x.AlphaInstanceUpdateParameterSchema != nil { + x.AlphaInstanceUpdateParameterSchema = nil + } + } else { + if x.AlphaInstanceUpdateParameterSchema == nil { + x.AlphaInstanceUpdateParameterSchema = new(pkg4_runtime.RawExtension) + } + yym19 := z.DecBinary() + _ = yym19 + if false { + } else if z.HasExtensions() && z.DecExt(x.AlphaInstanceUpdateParameterSchema) { + } else if !yym19 && z.IsJSONHandle() { + z.DecJSONUnmarshal(x.AlphaInstanceUpdateParameterSchema) + } else { + z.DecFallback(x.AlphaInstanceUpdateParameterSchema, false) + } + } + case "alphaBindingCreateParameterSchema": + if r.TryDecodeAsNil() { + if x.AlphaBindingCreateParameterSchema != nil { + x.AlphaBindingCreateParameterSchema = nil + } + } else { + if x.AlphaBindingCreateParameterSchema == nil { + x.AlphaBindingCreateParameterSchema = new(pkg4_runtime.RawExtension) + } + yym21 := z.DecBinary() + _ = yym21 + if false { + } else if z.HasExtensions() && z.DecExt(x.AlphaBindingCreateParameterSchema) { + } else if !yym21 && z.IsJSONHandle() { + z.DecJSONUnmarshal(x.AlphaBindingCreateParameterSchema) + } else { + z.DecFallback(x.AlphaBindingCreateParameterSchema, false) + } + } default: z.DecStructFieldNotFound(-1, yys3) } // end switch yys3 @@ -3198,16 +3552,16 @@ func (x *ServicePlan) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { var h codecSelfer1234 z, r := codec1978.GenHelperDecoder(d) _, _, _ = h, z, r - var yyj16 int - var yyb16 bool - var yyhl16 bool = l >= 0 - yyj16++ - if yyhl16 { - yyb16 = yyj16 > l + var yyj22 int + var yyb22 bool + var yyhl22 bool = l >= 0 + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l } else { - yyb16 = r.CheckBreak() + yyb22 = r.CheckBreak() } - if yyb16 { + if yyb22 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -3215,21 +3569,21 @@ func (x *ServicePlan) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.Name = "" } else { - yyv17 := &x.Name - yym18 := z.DecBinary() - _ = yym18 + yyv23 := &x.Name + yym24 := z.DecBinary() + _ = yym24 if false { } else { - *((*string)(yyv17)) = r.DecodeString() + *((*string)(yyv23)) = r.DecodeString() } } - yyj16++ - if yyhl16 { - yyb16 = yyj16 > l + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l } else { - yyb16 = r.CheckBreak() + yyb22 = r.CheckBreak() } - if yyb16 { + if yyb22 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -3237,21 +3591,21 @@ func (x *ServicePlan) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.ExternalID = "" } else { - yyv19 := &x.ExternalID - yym20 := z.DecBinary() - _ = yym20 + yyv25 := &x.ExternalID + yym26 := z.DecBinary() + _ = yym26 if false { } else { - *((*string)(yyv19)) = r.DecodeString() + *((*string)(yyv25)) = r.DecodeString() } } - yyj16++ - if yyhl16 { - yyb16 = yyj16 > l + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l } else { - yyb16 = r.CheckBreak() + yyb22 = r.CheckBreak() } - if yyb16 { + if yyb22 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -3259,21 +3613,21 @@ func (x *ServicePlan) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.Description = "" } else { - yyv21 := &x.Description - yym22 := z.DecBinary() - _ = yym22 + yyv27 := &x.Description + yym28 := z.DecBinary() + _ = yym28 if false { } else { - *((*string)(yyv21)) = r.DecodeString() + *((*string)(yyv27)) = r.DecodeString() } } - yyj16++ - if yyhl16 { - yyb16 = yyj16 > l + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l } else { - yyb16 = r.CheckBreak() + yyb22 = r.CheckBreak() } - if yyb16 { + if yyb22 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -3286,20 +3640,20 @@ func (x *ServicePlan) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Bindable == nil { x.Bindable = new(bool) } - yym24 := z.DecBinary() - _ = yym24 - if false { + yym30 := z.DecBinary() + _ = yym30 + if false { } else { *((*bool)(x.Bindable)) = r.DecodeBool() } } - yyj16++ - if yyhl16 { - yyb16 = yyj16 > l + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l } else { - yyb16 = r.CheckBreak() + yyb22 = r.CheckBreak() } - if yyb16 { + if yyb22 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -3307,21 +3661,21 @@ func (x *ServicePlan) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.Free = false } else { - yyv25 := &x.Free - yym26 := z.DecBinary() - _ = yym26 + yyv31 := &x.Free + yym32 := z.DecBinary() + _ = yym32 if false { } else { - *((*bool)(yyv25)) = r.DecodeBool() + *((*bool)(yyv31)) = r.DecodeBool() } } - yyj16++ - if yyhl16 { - yyb16 = yyj16 > l + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l } else { - yyb16 = r.CheckBreak() + yyb22 = r.CheckBreak() } - if yyb16 { + if yyb22 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -3334,28 +3688,115 @@ func (x *ServicePlan) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.ExternalMetadata == nil { x.ExternalMetadata = new(pkg4_runtime.RawExtension) } - yym28 := z.DecBinary() - _ = yym28 + yym34 := z.DecBinary() + _ = yym34 if false { } else if z.HasExtensions() && z.DecExt(x.ExternalMetadata) { - } else if !yym28 && z.IsJSONHandle() { + } else if !yym34 && z.IsJSONHandle() { z.DecJSONUnmarshal(x.ExternalMetadata) } else { z.DecFallback(x.ExternalMetadata, false) } } + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l + } else { + yyb22 = r.CheckBreak() + } + if yyb22 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.AlphaInstanceCreateParameterSchema != nil { + x.AlphaInstanceCreateParameterSchema = nil + } + } else { + if x.AlphaInstanceCreateParameterSchema == nil { + x.AlphaInstanceCreateParameterSchema = new(pkg4_runtime.RawExtension) + } + yym36 := z.DecBinary() + _ = yym36 + if false { + } else if z.HasExtensions() && z.DecExt(x.AlphaInstanceCreateParameterSchema) { + } else if !yym36 && z.IsJSONHandle() { + z.DecJSONUnmarshal(x.AlphaInstanceCreateParameterSchema) + } else { + z.DecFallback(x.AlphaInstanceCreateParameterSchema, false) + } + } + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l + } else { + yyb22 = r.CheckBreak() + } + if yyb22 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.AlphaInstanceUpdateParameterSchema != nil { + x.AlphaInstanceUpdateParameterSchema = nil + } + } else { + if x.AlphaInstanceUpdateParameterSchema == nil { + x.AlphaInstanceUpdateParameterSchema = new(pkg4_runtime.RawExtension) + } + yym38 := z.DecBinary() + _ = yym38 + if false { + } else if z.HasExtensions() && z.DecExt(x.AlphaInstanceUpdateParameterSchema) { + } else if !yym38 && z.IsJSONHandle() { + z.DecJSONUnmarshal(x.AlphaInstanceUpdateParameterSchema) + } else { + z.DecFallback(x.AlphaInstanceUpdateParameterSchema, false) + } + } + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l + } else { + yyb22 = r.CheckBreak() + } + if yyb22 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.AlphaBindingCreateParameterSchema != nil { + x.AlphaBindingCreateParameterSchema = nil + } + } else { + if x.AlphaBindingCreateParameterSchema == nil { + x.AlphaBindingCreateParameterSchema = new(pkg4_runtime.RawExtension) + } + yym40 := z.DecBinary() + _ = yym40 + if false { + } else if z.HasExtensions() && z.DecExt(x.AlphaBindingCreateParameterSchema) { + } else if !yym40 && z.IsJSONHandle() { + z.DecJSONUnmarshal(x.AlphaBindingCreateParameterSchema) + } else { + z.DecFallback(x.AlphaBindingCreateParameterSchema, false) + } + } for { - yyj16++ - if yyhl16 { - yyb16 = yyj16 > l + yyj22++ + if yyhl22 { + yyb22 = yyj22 > l } else { - yyb16 = r.CheckBreak() + yyb22 = r.CheckBreak() } - if yyb16 { + if yyb22 { break } z.DecSendContainerState(codecSelfer_containerArrayElem1234) - z.DecStructFieldNotFound(yyj16-1, "") + z.DecStructFieldNotFound(yyj22-1, "") } z.DecSendContainerState(codecSelfer_containerArrayEnd1234) } @@ -6095,13 +6536,14 @@ func (x *BindingSpec) CodecEncodeSelf(e *codec1978.Encoder) { } else { yysep2 := !z.EncBinary() yy2arr2 := z.EncBasicHandle().StructToArray - var yyq2 [4]bool + var yyq2 [5]bool _, _, _ = yysep2, yyq2, yy2arr2 const yyr2 bool = false yyq2[1] = x.Parameters != nil + yyq2[4] = x.AlphaPodPresetTemplate != nil var yynn2 int if yyr2 || yy2arr2 { - r.EncodeArrayStart(4) + r.EncodeArrayStart(5) } else { yynn2 = 3 for _, b := range yyq2 { @@ -6200,6 +6642,29 @@ func (x *BindingSpec) CodecEncodeSelf(e *codec1978.Encoder) { r.EncodeString(codecSelferC_UTF81234, string(x.ExternalID)) } } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + if yyq2[4] { + if x.AlphaPodPresetTemplate == nil { + r.EncodeNil() + } else { + x.AlphaPodPresetTemplate.CodecEncodeSelf(e) + } + } else { + r.EncodeNil() + } + } else { + if yyq2[4] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("alphaPodPresetTemplate")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.AlphaPodPresetTemplate == nil { + r.EncodeNil() + } else { + x.AlphaPodPresetTemplate.CodecEncodeSelf(e) + } + } + } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayEnd1234) } else { @@ -6311,6 +6776,17 @@ func (x *BindingSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { *((*string)(yyv9)) = r.DecodeString() } } + case "alphaPodPresetTemplate": + if r.TryDecodeAsNil() { + if x.AlphaPodPresetTemplate != nil { + x.AlphaPodPresetTemplate = nil + } + } else { + if x.AlphaPodPresetTemplate == nil { + x.AlphaPodPresetTemplate = new(AlphaPodPresetTemplate) + } + x.AlphaPodPresetTemplate.CodecDecodeSelf(d) + } default: z.DecStructFieldNotFound(-1, yys3) } // end switch yys3 @@ -6322,16 +6798,16 @@ func (x *BindingSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { var h codecSelfer1234 z, r := codec1978.GenHelperDecoder(d) _, _, _ = h, z, r - var yyj11 int - var yyb11 bool - var yyhl11 bool = l >= 0 - yyj11++ - if yyhl11 { - yyb11 = yyj11 > l + var yyj12 int + var yyb12 bool + var yyhl12 bool = l >= 0 + yyj12++ + if yyhl12 { + yyb12 = yyj12 > l } else { - yyb11 = r.CheckBreak() + yyb12 = r.CheckBreak() } - if yyb11 { + if yyb12 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -6339,16 +6815,16 @@ func (x *BindingSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.InstanceRef = pkg3_v1.LocalObjectReference{} } else { - yyv12 := &x.InstanceRef - yyv12.CodecDecodeSelf(d) + yyv13 := &x.InstanceRef + yyv13.CodecDecodeSelf(d) } - yyj11++ - if yyhl11 { - yyb11 = yyj11 > l + yyj12++ + if yyhl12 { + yyb12 = yyj12 > l } else { - yyb11 = r.CheckBreak() + yyb12 = r.CheckBreak() } - if yyb11 { + if yyb12 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -6361,23 +6837,23 @@ func (x *BindingSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Parameters == nil { x.Parameters = new(pkg4_runtime.RawExtension) } - yym14 := z.DecBinary() - _ = yym14 + yym15 := z.DecBinary() + _ = yym15 if false { } else if z.HasExtensions() && z.DecExt(x.Parameters) { - } else if !yym14 && z.IsJSONHandle() { + } else if !yym15 && z.IsJSONHandle() { z.DecJSONUnmarshal(x.Parameters) } else { z.DecFallback(x.Parameters, false) } } - yyj11++ - if yyhl11 { - yyb11 = yyj11 > l + yyj12++ + if yyhl12 { + yyb12 = yyj12 > l } else { - yyb11 = r.CheckBreak() + yyb12 = r.CheckBreak() } - if yyb11 { + if yyb12 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -6385,21 +6861,21 @@ func (x *BindingSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.SecretName = "" } else { - yyv15 := &x.SecretName - yym16 := z.DecBinary() - _ = yym16 + yyv16 := &x.SecretName + yym17 := z.DecBinary() + _ = yym17 if false { } else { - *((*string)(yyv15)) = r.DecodeString() + *((*string)(yyv16)) = r.DecodeString() } } - yyj11++ - if yyhl11 { - yyb11 = yyj11 > l + yyj12++ + if yyhl12 { + yyb12 = yyj12 > l } else { - yyb11 = r.CheckBreak() + yyb12 = r.CheckBreak() } - if yyb11 { + if yyb12 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -6407,26 +6883,280 @@ func (x *BindingSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.ExternalID = "" } else { - yyv17 := &x.ExternalID - yym18 := z.DecBinary() - _ = yym18 + yyv18 := &x.ExternalID + yym19 := z.DecBinary() + _ = yym19 if false { } else { - *((*string)(yyv17)) = r.DecodeString() + *((*string)(yyv18)) = r.DecodeString() + } + } + yyj12++ + if yyhl12 { + yyb12 = yyj12 > l + } else { + yyb12 = r.CheckBreak() + } + if yyb12 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.AlphaPodPresetTemplate != nil { + x.AlphaPodPresetTemplate = nil + } + } else { + if x.AlphaPodPresetTemplate == nil { + x.AlphaPodPresetTemplate = new(AlphaPodPresetTemplate) } + x.AlphaPodPresetTemplate.CodecDecodeSelf(d) } for { - yyj11++ - if yyhl11 { - yyb11 = yyj11 > l + yyj12++ + if yyhl12 { + yyb12 = yyj12 > l } else { - yyb11 = r.CheckBreak() + yyb12 = r.CheckBreak() } - if yyb11 { + if yyb12 { break } z.DecSendContainerState(codecSelfer_containerArrayElem1234) - z.DecStructFieldNotFound(yyj11-1, "") + z.DecStructFieldNotFound(yyj12-1, "") + } + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) +} + +func (x *AlphaPodPresetTemplate) CodecEncodeSelf(e *codec1978.Encoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperEncoder(e) + _, _, _ = h, z, r + if x == nil { + r.EncodeNil() + } else { + yym1 := z.EncBinary() + _ = yym1 + if false { + } else if z.HasExtensions() && z.EncExt(x) { + } else { + yysep2 := !z.EncBinary() + yy2arr2 := z.EncBasicHandle().StructToArray + var yyq2 [2]bool + _, _, _ = yysep2, yyq2, yy2arr2 + const yyr2 bool = false + var yynn2 int + if yyr2 || yy2arr2 { + r.EncodeArrayStart(2) + } else { + yynn2 = 2 + for _, b := range yyq2 { + if b { + yynn2++ + } + } + r.EncodeMapStart(yynn2) + yynn2 = 0 + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + yym4 := z.EncBinary() + _ = yym4 + if false { + } else { + r.EncodeString(codecSelferC_UTF81234, string(x.Name)) + } + } else { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("name")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + yym5 := z.EncBinary() + _ = yym5 + if false { + } else { + r.EncodeString(codecSelferC_UTF81234, string(x.Name)) + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + yy7 := &x.Selector + yym8 := z.EncBinary() + _ = yym8 + if false { + } else if z.HasExtensions() && z.EncExt(yy7) { + } else { + z.EncFallback(yy7) + } + } else { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("selector")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + yy9 := &x.Selector + yym10 := z.EncBinary() + _ = yym10 + if false { + } else if z.HasExtensions() && z.EncExt(yy9) { + } else { + z.EncFallback(yy9) + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayEnd1234) + } else { + z.EncSendContainerState(codecSelfer_containerMapEnd1234) + } + } + } +} + +func (x *AlphaPodPresetTemplate) CodecDecodeSelf(d *codec1978.Decoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperDecoder(d) + _, _, _ = h, z, r + yym1 := z.DecBinary() + _ = yym1 + if false { + } else if z.HasExtensions() && z.DecExt(x) { + } else { + yyct2 := r.ContainerType() + if yyct2 == codecSelferValueTypeMap1234 { + yyl2 := r.ReadMapStart() + if yyl2 == 0 { + z.DecSendContainerState(codecSelfer_containerMapEnd1234) + } else { + x.codecDecodeSelfFromMap(yyl2, d) + } + } else if yyct2 == codecSelferValueTypeArray1234 { + yyl2 := r.ReadArrayStart() + if yyl2 == 0 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + } else { + x.codecDecodeSelfFromArray(yyl2, d) + } + } else { + panic(codecSelferOnlyMapOrArrayEncodeToStructErr1234) + } + } +} + +func (x *AlphaPodPresetTemplate) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperDecoder(d) + _, _, _ = h, z, r + var yys3Slc = z.DecScratchBuffer() // default slice to decode into + _ = yys3Slc + var yyhl3 bool = l >= 0 + for yyj3 := 0; ; yyj3++ { + if yyhl3 { + if yyj3 >= l { + break + } + } else { + if r.CheckBreak() { + break + } + } + z.DecSendContainerState(codecSelfer_containerMapKey1234) + yys3Slc = r.DecodeBytes(yys3Slc, true, true) + yys3 := string(yys3Slc) + z.DecSendContainerState(codecSelfer_containerMapValue1234) + switch yys3 { + case "name": + if r.TryDecodeAsNil() { + x.Name = "" + } else { + yyv4 := &x.Name + yym5 := z.DecBinary() + _ = yym5 + if false { + } else { + *((*string)(yyv4)) = r.DecodeString() + } + } + case "selector": + if r.TryDecodeAsNil() { + x.Selector = pkg1_v1.LabelSelector{} + } else { + yyv6 := &x.Selector + yym7 := z.DecBinary() + _ = yym7 + if false { + } else if z.HasExtensions() && z.DecExt(yyv6) { + } else { + z.DecFallback(yyv6, false) + } + } + default: + z.DecStructFieldNotFound(-1, yys3) + } // end switch yys3 + } // end for yyj3 + z.DecSendContainerState(codecSelfer_containerMapEnd1234) +} + +func (x *AlphaPodPresetTemplate) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { + var h codecSelfer1234 + z, r := codec1978.GenHelperDecoder(d) + _, _, _ = h, z, r + var yyj8 int + var yyb8 bool + var yyhl8 bool = l >= 0 + yyj8++ + if yyhl8 { + yyb8 = yyj8 > l + } else { + yyb8 = r.CheckBreak() + } + if yyb8 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + x.Name = "" + } else { + yyv9 := &x.Name + yym10 := z.DecBinary() + _ = yym10 + if false { + } else { + *((*string)(yyv9)) = r.DecodeString() + } + } + yyj8++ + if yyhl8 { + yyb8 = yyj8 > l + } else { + yyb8 = r.CheckBreak() + } + if yyb8 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + x.Selector = pkg1_v1.LabelSelector{} + } else { + yyv11 := &x.Selector + yym12 := z.DecBinary() + _ = yym12 + if false { + } else if z.HasExtensions() && z.DecExt(yyv11) { + } else { + z.DecFallback(yyv11, false) + } + } + for { + yyj8++ + if yyhl8 { + yyb8 = yyj8 > l + } else { + yyb8 = r.CheckBreak() + } + if yyb8 { + break + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + z.DecStructFieldNotFound(yyj8-1, "") } z.DecSendContainerState(codecSelfer_containerArrayEnd1234) } @@ -7481,7 +8211,7 @@ func (x codecSelfer1234) decSliceServicePlan(v *[]ServicePlan, d *codec1978.Deco yyrg1 := len(yyv1) > 0 yyv21 := yyv1 - yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 72) + yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 96) if yyrt1 { if yyrl1 <= cap(yyv1) { yyv1 = yyv1[:yyrl1] @@ -7838,7 +8568,7 @@ func (x codecSelfer1234) decSliceBinding(v *[]Binding, d *codec1978.Decoder) { yyrg1 := len(yyv1) > 0 yyv21 := yyv1 - yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 344) + yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 352) if yyrt1 { if yyrl1 <= cap(yyv1) { yyv1 = yyv1[:yyrl1] diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.go index ae13e9ca4b65..251bfa14bfbb 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/types.go @@ -49,9 +49,21 @@ type BrokerSpec struct { // URL is the address used to communicate with the Broker. URL string `json:"url"` - // AuthSecret is a reference to a Secret containing auth information the - // catalog should use to authenticate to this Broker. - AuthSecret *v1.ObjectReference `json:"authSecret,omitempty"` + // AuthInfo contains the data that the service catalog should use to authenticate + // with the Broker. + AuthInfo *BrokerAuthInfo `json:"authInfo,omitempty"` +} + +// BrokerAuthInfo is a union type that contains information on one of the authentication methods +// the the service catalog and brokers may support, according to the OpenServiceBroker API +// specification (https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md). +// +// Note that we currently restrict a single broker to have only one of these fields +// set on it. +type BrokerAuthInfo struct { + // BasicAuthSecret is a reference to a Secret containing auth information the + // catalog should use to authenticate to this Broker using basic auth. + BasicAuthSecret *v1.ObjectReference `json:"basicAuthSecret,omitempty"` } // BrokerStatus represents the current status of a Broker. @@ -63,14 +75,18 @@ type BrokerStatus struct { type BrokerCondition struct { // Type of the condition, currently ('Ready'). Type BrokerConditionType `json:"type"` + // Status of the condition, one of ('True', 'False', 'Unknown'). Status ConditionStatus `json:"status"` + // LastTransitionTime is the timestamp corresponding to the last status // change of this condition. LastTransitionTime metav1.Time `json:"lastTransitionTime"` + // Reason is a brief machine readable explanation for the condition's last // transition. Reason string `json:"reason"` + // Message is a human readable description of the details of the last // transition, complementing reason. Message string `json:"message"` @@ -96,8 +112,10 @@ type ConditionStatus string const ( // ConditionTrue represents the fact that a given condition is true ConditionTrue ConditionStatus = "True" + // ConditionFalse represents the fact that a given condition is false ConditionFalse ConditionStatus = "False" + // ConditionUnknown represents the fact that a given condition is unknown ConditionUnknown ConditionStatus = "Unknown" ) @@ -194,6 +212,28 @@ type ServicePlan struct { // user-facing content and display instructions. This field may contain // platform-specific conventional values. ExternalMetadata *runtime.RawExtension `json:"externalMetadata, omitempty"` + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaInstanceCreateParameterSchema is the schema for the parameters + // that may be supplied when provisioning a new Instance on this plan. + AlphaInstanceCreateParameterSchema *runtime.RawExtension `json:"alphaInstanceCreateParameterSchema,omitempty"` + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaInstanceUpdateParameterSchema is the schema for the parameters + // that may be updated once an Instance has been provisioned on this plan. + // This field only has meaning if the ServiceClass is PlanUpdatable. + AlphaInstanceUpdateParameterSchema *runtime.RawExtension `json:"alphaInstanceUpdateParameterSchema,omitempty"` + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaBindingCreateParameterSchema is the schema for the parameters that + // may be supplied binding to an Instance on this plan. + AlphaBindingCreateParameterSchema *runtime.RawExtension `json:"alphaBindingCreateParameterSchema,omitempty"` } // InstanceList is a list of instances. @@ -265,14 +305,18 @@ type InstanceStatus struct { type InstanceCondition struct { // Type of the condition, currently ('Ready'). Type InstanceConditionType `json:"type"` + // Status of the condition, one of ('True', 'False', 'Unknown'). Status ConditionStatus `json:"status"` + // LastTransitionTime is the timestamp corresponding to the last status // change of this condition. LastTransitionTime metav1.Time `json:"lastTransitionTime"` + // Reason is a brief machine readable explanation for the condition's last // transition. Reason string `json:"reason"` + // Message is a human readable description of the details of the last // transition, complementing reason. Message string `json:"message"` @@ -326,6 +370,28 @@ type BindingSpec struct { // // Immutable. ExternalID string `json:"externalID"` + + // Currently, this field is ALPHA: it may change or disappear at any time + // and its data will not be migrated. + // + // AlphaPodPresetTemplate describes how a PodPreset should be created once + // the Binding has been made. If supplied, a PodPreset will be created + // using information in this field once the Binding has been made in the + // Broker. The PodPreset will use the EnvFrom feature to expose the keys + // from the Secret (specified by SecretName) that holds the Binding + // information into Pods. + // + // In the future, we will provide a higher degree of control over the PodPreset. + AlphaPodPresetTemplate *AlphaPodPresetTemplate `json:"alphaPodPresetTemplate,omitempty"` +} + +// AlphaPodPresetTemplate represents how a PodPreset should be created for a +// Binding. +type AlphaPodPresetTemplate struct { + // Name is the name of the PodPreset to create. + Name string `json:"name"` + // Selector is the LabelSelector of the PodPreset to create. + Selector metav1.LabelSelector `json:"selector"` } // BindingStatus represents the current status of a Binding. @@ -341,14 +407,18 @@ type BindingStatus struct { type BindingCondition struct { // Type of the condition, currently ('Ready'). Type BindingConditionType `json:"type"` + // Status of the condition, one of ('True', 'False', 'Unknown'). Status ConditionStatus `json:"status"` + // LastTransitionTime is the timestamp corresponding to the last status // change of this condition. LastTransitionTime metav1.Time `json:"lastTransitionTime"` + // Reason is a brief machine readable explanation for the condition's last // transition. Reason string `json:"reason"` + // Message is a human readable description of the details of the last // transition, complementing reason. Message string `json:"message"` @@ -361,3 +431,8 @@ const ( // BindingConditionReady represents a binding condition is in ready state. BindingConditionReady BindingConditionType = "Ready" ) + +// These are external finalizer values to service catalog, must be qualified name. +const ( + FinalizerServiceCatalog string = "kubernetes-incubator/service-catalog" +) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.conversion.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.conversion.go index 10021d1cb625..243b771180ee 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.conversion.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.conversion.go @@ -36,6 +36,8 @@ func init() { // Public to allow building arbitrary schemes. func RegisterConversions(scheme *runtime.Scheme) error { return scheme.AddGeneratedConversionFuncs( + Convert_v1alpha1_AlphaPodPresetTemplate_To_servicecatalog_AlphaPodPresetTemplate, + Convert_servicecatalog_AlphaPodPresetTemplate_To_v1alpha1_AlphaPodPresetTemplate, Convert_v1alpha1_Binding_To_servicecatalog_Binding, Convert_servicecatalog_Binding_To_v1alpha1_Binding, Convert_v1alpha1_BindingCondition_To_servicecatalog_BindingCondition, @@ -48,6 +50,8 @@ func RegisterConversions(scheme *runtime.Scheme) error { Convert_servicecatalog_BindingStatus_To_v1alpha1_BindingStatus, Convert_v1alpha1_Broker_To_servicecatalog_Broker, Convert_servicecatalog_Broker_To_v1alpha1_Broker, + Convert_v1alpha1_BrokerAuthInfo_To_servicecatalog_BrokerAuthInfo, + Convert_servicecatalog_BrokerAuthInfo_To_v1alpha1_BrokerAuthInfo, Convert_v1alpha1_BrokerCondition_To_servicecatalog_BrokerCondition, Convert_servicecatalog_BrokerCondition_To_v1alpha1_BrokerCondition, Convert_v1alpha1_BrokerList_To_servicecatalog_BrokerList, @@ -75,6 +79,26 @@ func RegisterConversions(scheme *runtime.Scheme) error { ) } +func autoConvert_v1alpha1_AlphaPodPresetTemplate_To_servicecatalog_AlphaPodPresetTemplate(in *AlphaPodPresetTemplate, out *servicecatalog.AlphaPodPresetTemplate, s conversion.Scope) error { + out.Name = in.Name + out.Selector = in.Selector + return nil +} + +func Convert_v1alpha1_AlphaPodPresetTemplate_To_servicecatalog_AlphaPodPresetTemplate(in *AlphaPodPresetTemplate, out *servicecatalog.AlphaPodPresetTemplate, s conversion.Scope) error { + return autoConvert_v1alpha1_AlphaPodPresetTemplate_To_servicecatalog_AlphaPodPresetTemplate(in, out, s) +} + +func autoConvert_servicecatalog_AlphaPodPresetTemplate_To_v1alpha1_AlphaPodPresetTemplate(in *servicecatalog.AlphaPodPresetTemplate, out *AlphaPodPresetTemplate, s conversion.Scope) error { + out.Name = in.Name + out.Selector = in.Selector + return nil +} + +func Convert_servicecatalog_AlphaPodPresetTemplate_To_v1alpha1_AlphaPodPresetTemplate(in *servicecatalog.AlphaPodPresetTemplate, out *AlphaPodPresetTemplate, s conversion.Scope) error { + return autoConvert_servicecatalog_AlphaPodPresetTemplate_To_v1alpha1_AlphaPodPresetTemplate(in, out, s) +} + func autoConvert_v1alpha1_Binding_To_servicecatalog_Binding(in *Binding, out *servicecatalog.Binding, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha1_BindingSpec_To_servicecatalog_BindingSpec(&in.Spec, &out.Spec, s); err != nil { @@ -156,6 +180,7 @@ func autoConvert_v1alpha1_BindingSpec_To_servicecatalog_BindingSpec(in *BindingS out.Parameters = (*runtime.RawExtension)(unsafe.Pointer(in.Parameters)) out.SecretName = in.SecretName out.ExternalID = in.ExternalID + out.AlphaPodPresetTemplate = (*servicecatalog.AlphaPodPresetTemplate)(unsafe.Pointer(in.AlphaPodPresetTemplate)) return nil } @@ -168,6 +193,7 @@ func autoConvert_servicecatalog_BindingSpec_To_v1alpha1_BindingSpec(in *servicec out.Parameters = (*runtime.RawExtension)(unsafe.Pointer(in.Parameters)) out.SecretName = in.SecretName out.ExternalID = in.ExternalID + out.AlphaPodPresetTemplate = (*AlphaPodPresetTemplate)(unsafe.Pointer(in.AlphaPodPresetTemplate)) return nil } @@ -225,6 +251,24 @@ func Convert_servicecatalog_Broker_To_v1alpha1_Broker(in *servicecatalog.Broker, return autoConvert_servicecatalog_Broker_To_v1alpha1_Broker(in, out, s) } +func autoConvert_v1alpha1_BrokerAuthInfo_To_servicecatalog_BrokerAuthInfo(in *BrokerAuthInfo, out *servicecatalog.BrokerAuthInfo, s conversion.Scope) error { + out.BasicAuthSecret = (*v1.ObjectReference)(unsafe.Pointer(in.BasicAuthSecret)) + return nil +} + +func Convert_v1alpha1_BrokerAuthInfo_To_servicecatalog_BrokerAuthInfo(in *BrokerAuthInfo, out *servicecatalog.BrokerAuthInfo, s conversion.Scope) error { + return autoConvert_v1alpha1_BrokerAuthInfo_To_servicecatalog_BrokerAuthInfo(in, out, s) +} + +func autoConvert_servicecatalog_BrokerAuthInfo_To_v1alpha1_BrokerAuthInfo(in *servicecatalog.BrokerAuthInfo, out *BrokerAuthInfo, s conversion.Scope) error { + out.BasicAuthSecret = (*v1.ObjectReference)(unsafe.Pointer(in.BasicAuthSecret)) + return nil +} + +func Convert_servicecatalog_BrokerAuthInfo_To_v1alpha1_BrokerAuthInfo(in *servicecatalog.BrokerAuthInfo, out *BrokerAuthInfo, s conversion.Scope) error { + return autoConvert_servicecatalog_BrokerAuthInfo_To_v1alpha1_BrokerAuthInfo(in, out, s) +} + func autoConvert_v1alpha1_BrokerCondition_To_servicecatalog_BrokerCondition(in *BrokerCondition, out *servicecatalog.BrokerCondition, s conversion.Scope) error { out.Type = servicecatalog.BrokerConditionType(in.Type) out.Status = servicecatalog.ConditionStatus(in.Status) @@ -273,7 +317,7 @@ func Convert_servicecatalog_BrokerList_To_v1alpha1_BrokerList(in *servicecatalog func autoConvert_v1alpha1_BrokerSpec_To_servicecatalog_BrokerSpec(in *BrokerSpec, out *servicecatalog.BrokerSpec, s conversion.Scope) error { out.URL = in.URL - out.AuthSecret = (*v1.ObjectReference)(unsafe.Pointer(in.AuthSecret)) + out.AuthInfo = (*servicecatalog.BrokerAuthInfo)(unsafe.Pointer(in.AuthInfo)) return nil } @@ -283,7 +327,7 @@ func Convert_v1alpha1_BrokerSpec_To_servicecatalog_BrokerSpec(in *BrokerSpec, ou func autoConvert_servicecatalog_BrokerSpec_To_v1alpha1_BrokerSpec(in *servicecatalog.BrokerSpec, out *BrokerSpec, s conversion.Scope) error { out.URL = in.URL - out.AuthSecret = (*v1.ObjectReference)(unsafe.Pointer(in.AuthSecret)) + out.AuthInfo = (*BrokerAuthInfo)(unsafe.Pointer(in.AuthInfo)) return nil } @@ -498,6 +542,9 @@ func autoConvert_v1alpha1_ServicePlan_To_servicecatalog_ServicePlan(in *ServiceP out.Bindable = (*bool)(unsafe.Pointer(in.Bindable)) out.Free = in.Free out.ExternalMetadata = (*runtime.RawExtension)(unsafe.Pointer(in.ExternalMetadata)) + out.AlphaInstanceCreateParameterSchema = (*runtime.RawExtension)(unsafe.Pointer(in.AlphaInstanceCreateParameterSchema)) + out.AlphaInstanceUpdateParameterSchema = (*runtime.RawExtension)(unsafe.Pointer(in.AlphaInstanceUpdateParameterSchema)) + out.AlphaBindingCreateParameterSchema = (*runtime.RawExtension)(unsafe.Pointer(in.AlphaBindingCreateParameterSchema)) return nil } @@ -512,6 +559,9 @@ func autoConvert_servicecatalog_ServicePlan_To_v1alpha1_ServicePlan(in *servicec out.Bindable = (*bool)(unsafe.Pointer(in.Bindable)) out.Free = in.Free out.ExternalMetadata = (*runtime.RawExtension)(unsafe.Pointer(in.ExternalMetadata)) + out.AlphaInstanceCreateParameterSchema = (*runtime.RawExtension)(unsafe.Pointer(in.AlphaInstanceCreateParameterSchema)) + out.AlphaInstanceUpdateParameterSchema = (*runtime.RawExtension)(unsafe.Pointer(in.AlphaInstanceUpdateParameterSchema)) + out.AlphaBindingCreateParameterSchema = (*runtime.RawExtension)(unsafe.Pointer(in.AlphaBindingCreateParameterSchema)) return nil } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go index a6ab07e39195..d4095875bfb4 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1/zz_generated.deepcopy.go @@ -36,12 +36,14 @@ func init() { // to allow building arbitrary schemes. func RegisterDeepCopies(scheme *runtime.Scheme) error { return scheme.AddGeneratedDeepCopyFuncs( + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_AlphaPodPresetTemplate, InType: reflect.TypeOf(&AlphaPodPresetTemplate{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_Binding, InType: reflect.TypeOf(&Binding{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BindingCondition, InType: reflect.TypeOf(&BindingCondition{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BindingList, InType: reflect.TypeOf(&BindingList{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BindingSpec, InType: reflect.TypeOf(&BindingSpec{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BindingStatus, InType: reflect.TypeOf(&BindingStatus{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_Broker, InType: reflect.TypeOf(&Broker{})}, + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BrokerAuthInfo, InType: reflect.TypeOf(&BrokerAuthInfo{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BrokerCondition, InType: reflect.TypeOf(&BrokerCondition{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BrokerList, InType: reflect.TypeOf(&BrokerList{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_BrokerSpec, InType: reflect.TypeOf(&BrokerSpec{})}, @@ -57,6 +59,20 @@ func RegisterDeepCopies(scheme *runtime.Scheme) error { ) } +func DeepCopy_v1alpha1_AlphaPodPresetTemplate(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*AlphaPodPresetTemplate) + out := out.(*AlphaPodPresetTemplate) + *out = *in + if newVal, err := c.DeepCopy(&in.Selector); err != nil { + return err + } else { + out.Selector = *newVal.(*v1.LabelSelector) + } + return nil + } +} + func DeepCopy_v1alpha1_Binding(in interface{}, out interface{}, c *conversion.Cloner) error { { in := in.(*Binding) @@ -118,6 +134,13 @@ func DeepCopy_v1alpha1_BindingSpec(in interface{}, out interface{}, c *conversio *out = newVal.(*runtime.RawExtension) } } + if in.AlphaPodPresetTemplate != nil { + in, out := &in.AlphaPodPresetTemplate, &out.AlphaPodPresetTemplate + *out = new(AlphaPodPresetTemplate) + if err := DeepCopy_v1alpha1_AlphaPodPresetTemplate(*in, *out, c); err != nil { + return err + } + } return nil } } @@ -165,6 +188,20 @@ func DeepCopy_v1alpha1_Broker(in interface{}, out interface{}, c *conversion.Clo } } +func DeepCopy_v1alpha1_BrokerAuthInfo(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*BrokerAuthInfo) + out := out.(*BrokerAuthInfo) + *out = *in + if in.BasicAuthSecret != nil { + in, out := &in.BasicAuthSecret, &out.BasicAuthSecret + *out = new(api_v1.ObjectReference) + **out = **in + } + return nil + } +} + func DeepCopy_v1alpha1_BrokerCondition(in interface{}, out interface{}, c *conversion.Cloner) error { { in := in.(*BrokerCondition) @@ -198,10 +235,12 @@ func DeepCopy_v1alpha1_BrokerSpec(in interface{}, out interface{}, c *conversion in := in.(*BrokerSpec) out := out.(*BrokerSpec) *out = *in - if in.AuthSecret != nil { - in, out := &in.AuthSecret, &out.AuthSecret - *out = new(api_v1.ObjectReference) - **out = **in + if in.AuthInfo != nil { + in, out := &in.AuthInfo, &out.AuthInfo + *out = new(BrokerAuthInfo) + if err := DeepCopy_v1alpha1_BrokerAuthInfo(*in, *out, c); err != nil { + return err + } } return nil } @@ -400,6 +439,30 @@ func DeepCopy_v1alpha1_ServicePlan(in interface{}, out interface{}, c *conversio *out = newVal.(*runtime.RawExtension) } } + if in.AlphaInstanceCreateParameterSchema != nil { + in, out := &in.AlphaInstanceCreateParameterSchema, &out.AlphaInstanceCreateParameterSchema + if newVal, err := c.DeepCopy(*in); err != nil { + return err + } else { + *out = newVal.(*runtime.RawExtension) + } + } + if in.AlphaInstanceUpdateParameterSchema != nil { + in, out := &in.AlphaInstanceUpdateParameterSchema, &out.AlphaInstanceUpdateParameterSchema + if newVal, err := c.DeepCopy(*in); err != nil { + return err + } else { + *out = newVal.(*runtime.RawExtension) + } + } + if in.AlphaBindingCreateParameterSchema != nil { + in, out := &in.AlphaBindingCreateParameterSchema, &out.AlphaBindingCreateParameterSchema + if newVal, err := c.DeepCopy(*in); err != nil { + return err + } else { + *out = newVal.(*runtime.RawExtension) + } + } return nil } } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding.go index c477d9f8cba6..1c008d3b7493 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding.go @@ -18,6 +18,7 @@ package validation import ( apivalidation "k8s.io/apimachinery/pkg/api/validation" + metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/util/validation/field" sc "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" @@ -52,6 +53,14 @@ func validateBindingSpec(spec *sc.BindingSpec, fldPath *field.Path, create bool) allErrs = append(allErrs, field.Invalid(fldPath.Child("secretName"), spec.SecretName, msg)) } + if spec.AlphaPodPresetTemplate != nil { + allErrs = append(allErrs, metav1validation.ValidateLabelSelector(&spec.AlphaPodPresetTemplate.Selector, fldPath.Child("alphaPodPresetTemplate", "selector"))...) + + for _, msg := range apivalidation.NameIsDNSSubdomain(spec.AlphaPodPresetTemplate.Name, false /* prefix */) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("alphaPodPresetTemplate", "name"), spec.AlphaPodPresetTemplate.Name, msg)) + } + } + return allErrs } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding_test.go index b007069c3101..0eab5effdf17 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/binding_test.go @@ -25,6 +25,21 @@ import ( "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" ) +func validBinding() *servicecatalog.Binding { + return &servicecatalog.Binding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-binding", + Namespace: "test-ns", + }, + Spec: servicecatalog.BindingSpec{ + InstanceRef: v1.LocalObjectReference{ + Name: "test-instance", + }, + SecretName: "test-secret", + }, + } +} + func TestValidateBinding(t *testing.T) { cases := []struct { name string @@ -32,94 +47,75 @@ func TestValidateBinding(t *testing.T) { valid bool }{ { - name: "valid", - binding: &servicecatalog.Binding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-binding", - Namespace: "test-ns", - }, - Spec: servicecatalog.BindingSpec{ - InstanceRef: v1.LocalObjectReference{ - Name: "test-instance", - }, - SecretName: "test-secret", - }, - }, - valid: true, + name: "valid", + binding: validBinding(), + valid: true, }, { name: "missing namespace", - binding: &servicecatalog.Binding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-binding", - }, - Spec: servicecatalog.BindingSpec{ - InstanceRef: v1.LocalObjectReference{ - Name: "test-instance", - }, - SecretName: "test-secret", - }, - }, + binding: func() *servicecatalog.Binding { + b := validBinding() + b.Namespace = "" + return b + }(), valid: false, }, { name: "missing instance name", - binding: &servicecatalog.Binding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-binding", - Namespace: "test-ns", - }, - Spec: servicecatalog.BindingSpec{ - SecretName: "test-secret", - }, - }, + binding: func() *servicecatalog.Binding { + b := validBinding() + b.Spec.InstanceRef.Name = "" + return b + }(), valid: false, }, { name: "invalid instance name", - binding: &servicecatalog.Binding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-binding", - Namespace: "test-ns", - }, - Spec: servicecatalog.BindingSpec{ - InstanceRef: v1.LocalObjectReference{ - Name: "test-instance-)*!", - }, - SecretName: "test-secret", - }, - }, + binding: func() *servicecatalog.Binding { + b := validBinding() + b.Spec.InstanceRef.Name = "test-instance-)*!" + return b + }(), valid: false, }, { name: "missing secretName", - binding: &servicecatalog.Binding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-binding", - Namespace: "test-ns", - }, - Spec: servicecatalog.BindingSpec{ - InstanceRef: v1.LocalObjectReference{ - Name: "test-instance", - }, - }, - }, + binding: func() *servicecatalog.Binding { + b := validBinding() + b.Spec.SecretName = "" + return b + }(), valid: false, }, { name: "invalid secretName", - binding: &servicecatalog.Binding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-binding", - Namespace: "test-ns", - }, - Spec: servicecatalog.BindingSpec{ - InstanceRef: v1.LocalObjectReference{ - Name: "test-instance", - }, - SecretName: "T_T", - }, - }, + binding: func() *servicecatalog.Binding { + b := validBinding() + b.Spec.SecretName = "T_T" + return b + }(), + valid: false, + }, + { + name: "invalid alphaPodPresetTemplate.name", + binding: func() *servicecatalog.Binding { + b := validBinding() + b.Spec.AlphaPodPresetTemplate = &servicecatalog.AlphaPodPresetTemplate{ + Name: "T_T", + } + return b + }(), + valid: false, + }, + { + name: "invalid alphaPodPresetTemplate.selector", + binding: func() *servicecatalog.Binding { + b := validBinding() + b.Spec.AlphaPodPresetTemplate = &servicecatalog.AlphaPodPresetTemplate{ + Selector: metav1.LabelSelector{}, + } + return b + }(), valid: false, }, } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker.go index 3eb89dadc483..f78dfb4e01a4 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker.go @@ -49,13 +49,26 @@ func validateBrokerSpec(spec *sc.BrokerSpec, fldPath *field.Path) field.ErrorLis "brokers must have a remote url to contact")) } - if spec.AuthSecret != nil { - for _, msg := range apivalidation.ValidateNamespaceName(spec.AuthSecret.Namespace, false /* prefix */) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("authSecret", "namespace"), spec.AuthSecret.Namespace, msg)) - } - - for _, msg := range apivalidation.NameIsDNSSubdomain(spec.AuthSecret.Name, false /* prefix */) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("authSecret", "name"), spec.AuthSecret.Name, msg)) + // if there is auth information, check it to make sure that it's properly formatted + if spec.AuthInfo != nil { + // TODO: when we start supporting additional auth schemes, this code will have to accommodate + // the new schemes + basicAuthSecret := spec.AuthInfo.BasicAuthSecret + if basicAuthSecret != nil { + for _, msg := range apivalidation.ValidateNamespaceName(basicAuthSecret.Namespace, false /* prefix */) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("authInfo", "basicAuthSecret", "namespace"), basicAuthSecret.Namespace, msg)) + } + + for _, msg := range apivalidation.NameIsDNSSubdomain(basicAuthSecret.Name, false /* prefix */) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("authInfo", "basicAuthSecret", "name"), basicAuthSecret.Name, msg)) + } + } else { + // if there's no BasicAuthSecret, then we need to error because there are no other auth + // options right now + allErrs = append( + allErrs, + field.Required(fldPath.Child("authInfo", "basicAuthSecret"), "a basic auth secret is required"), + ) } } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker_test.go index 3430ac01437e..9b72a5a6747e 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/broker_test.go @@ -32,6 +32,8 @@ func TestValidateBroker(t *testing.T) { valid bool }{ { + // covers the case where there is no AuthInfo field specified. the validator should + // ignore the field and still succeed the validation name: "valid broker - no auth secret", broker: &servicecatalog.Broker{ ObjectMeta: metav1.ObjectMeta{ @@ -51,9 +53,11 @@ func TestValidateBroker(t *testing.T) { }, Spec: servicecatalog.BrokerSpec{ URL: "http://example.com", - AuthSecret: &v1.ObjectReference{ - Namespace: "test-ns", - Name: "test-secret", + AuthInfo: &servicecatalog.BrokerAuthInfo{ + BasicAuthSecret: &v1.ObjectReference{ + Namespace: "test-ns", + Name: "test-secret", + }, }, }, }, @@ -80,8 +84,10 @@ func TestValidateBroker(t *testing.T) { }, Spec: servicecatalog.BrokerSpec{ URL: "http://example.com", - AuthSecret: &v1.ObjectReference{ - Name: "test-secret", + AuthInfo: &servicecatalog.BrokerAuthInfo{ + BasicAuthSecret: &v1.ObjectReference{ + Name: "test-secret", + }, }, }, }, @@ -95,8 +101,10 @@ func TestValidateBroker(t *testing.T) { }, Spec: servicecatalog.BrokerSpec{ URL: "http://example.com", - AuthSecret: &v1.ObjectReference{ - Namespace: "test-ns", + AuthInfo: &servicecatalog.BrokerAuthInfo{ + BasicAuthSecret: &v1.ObjectReference{ + Namespace: "test-ns", + }, }, }, }, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance.go index ad665e0dc5fe..2c0997cc63a5 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance.go @@ -37,6 +37,7 @@ func internalValidateInstance(instance *sc.Instance, create bool) field.ErrorLis validateInstanceName, field.NewPath("metadata"))...) allErrs = append(allErrs, validateInstanceSpec(&instance.Spec, field.NewPath("Spec"), create)...) + allErrs = append(allErrs, validateInstanceStatus(&instance.Status, field.NewPath("Status"), create)...) return allErrs } @@ -62,15 +63,54 @@ func validateInstanceSpec(spec *sc.InstanceSpec, fldPath *field.Path, create boo return allErrs } +func validateInstanceStatus(spec *sc.InstanceStatus, fldPath *field.Path, create bool) field.ErrorList { + errors := field.ErrorList{} + // TODO(vaikas): Implement more comprehensive status validation. + // https://github.com/kubernetes-incubator/service-catalog/issues/882 + + // Do not allow the instance to be ready if an async operation is ongoing + // ongoing + if spec.AsyncOpInProgress { + for _, c := range spec.Conditions { + if c.Type == sc.InstanceConditionReady && c.Status == sc.ConditionTrue { + errors = append(errors, field.Forbidden(fldPath.Child("Conditions"), "Can not set InstanceConditionReady to true when an async operation is in progress")) + } + } + } + + return errors +} + +// internalValidateInstanceUpdateAllowed ensures there is not an asynchronous +// operation ongoing with the instance before allowing an update to go through. +func internalValidateInstanceUpdateAllowed(new *sc.Instance, old *sc.Instance) field.ErrorList { + errors := field.ErrorList{} + if old.Status.AsyncOpInProgress { + errors = append(errors, field.Forbidden(field.NewPath("Spec"), "Another operation for this service instance is in progress")) + } + return errors +} + // ValidateInstanceUpdate validates a change to the Instance's spec. func ValidateInstanceUpdate(new *sc.Instance, old *sc.Instance) field.ErrorList { - return internalValidateInstance(new, false) + allErrs := field.ErrorList{} + allErrs = append(allErrs, internalValidateInstanceUpdateAllowed(new, old)...) + allErrs = append(allErrs, internalValidateInstance(new, false)...) + return allErrs +} + +func internalValidateInstanceStatusUpdateAllowed(new *sc.Instance, old *sc.Instance) field.ErrorList { + errors := field.ErrorList{} + // TODO(vaikas): Are there any cases where we do not allow updates to + // Status during Async updates in progress? + return errors } // ValidateInstanceStatusUpdate checks that when changing from an older -// instance to a newer instance is okay. +// instance to a newer instance is okay. This only checks the instance.Status field. func ValidateInstanceStatusUpdate(new *sc.Instance, old *sc.Instance) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, ValidateInstanceUpdate(new, old)...) + allErrs = append(allErrs, internalValidateInstanceStatusUpdateAllowed(new, old)...) + allErrs = append(allErrs, internalValidateInstance(new, false)...) return allErrs } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance_test.go index 58e32bf49c5e..f22773b305c6 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/instance_test.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -123,3 +124,245 @@ func TestValidateInstance(t *testing.T) { } } } + +func TestValidateInstanceUpdate(t *testing.T) { + cases := []struct { + name string + old *servicecatalog.Instance + new *servicecatalog.Instance + valid bool + err string // Error string to match against if error expected + }{ + { + name: "no update with async op in progress", + old: &servicecatalog.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: servicecatalog.InstanceSpec{ + ServiceClassName: "test-serviceclass", + PlanName: "Test-Plan", + }, + Status: servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + }, + }, + new: &servicecatalog.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: servicecatalog.InstanceSpec{ + ServiceClassName: "test-serviceclass", + PlanName: "Test-Plan-2", + }, + Status: servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + }, + }, + valid: false, + err: "Another operation for this service instance is in progress", + }, + { + name: "allow update with no async op in progress", + old: &servicecatalog.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: servicecatalog.InstanceSpec{ + ServiceClassName: "test-serviceclass", + PlanName: "Test-Plan", + }, + Status: servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + }, + }, + new: &servicecatalog.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: servicecatalog.InstanceSpec{ + ServiceClassName: "test-serviceclass", + // TODO(vaikas): This does not actually update + // spec yet, but once it does, validate it changes. + PlanName: "Test-Plan-2", + }, + Status: servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + }, + }, + valid: true, + err: "", + }, + } + + for _, tc := range cases { + errs := ValidateInstanceUpdate(tc.new, tc.old) + if len(errs) != 0 && tc.valid { + t.Errorf("%v: unexpected error: %v", tc.name, errs) + continue + } else if len(errs) == 0 && !tc.valid { + t.Errorf("%v: unexpected success", tc.name) + } + if !tc.valid { + for _, err := range errs { + if !strings.Contains(err.Detail, tc.err) { + t.Errorf("Error %q did not contain expected message %q", err.Detail, tc.err) + } + } + } + } +} + +func TestValidateInstanceStatusUpdate(t *testing.T) { + cases := []struct { + name string + old *servicecatalog.InstanceStatus + new *servicecatalog.InstanceStatus + valid bool + err string // Error string to match against if error expected + }{ + { + name: "Start async op", + old: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + }, + new: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + }, + valid: true, + err: "", + }, + { + name: "Complete async op", + old: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + }, + new: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + }, + valid: true, + err: "", + }, + { + name: "InstanceConditionReady can not be true if async is ongoing", + old: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + Conditions: []servicecatalog.InstanceCondition{{ + Type: servicecatalog.InstanceConditionReady, + Status: servicecatalog.ConditionFalse, + }}, + }, + new: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + Conditions: []servicecatalog.InstanceCondition{{ + Type: servicecatalog.InstanceConditionReady, + Status: servicecatalog.ConditionTrue, + }}, + }, + valid: false, + err: "async operation is in progress", + }, + { + name: "InstanceConditionReady can be true if async is completed", + old: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + Conditions: []servicecatalog.InstanceCondition{{ + Type: servicecatalog.InstanceConditionReady, + Status: servicecatalog.ConditionFalse, + }}, + }, + new: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + Conditions: []servicecatalog.InstanceCondition{{ + Type: servicecatalog.InstanceConditionReady, + Status: servicecatalog.ConditionTrue, + }}, + }, + valid: true, + err: "", + }, + { + name: "Update instance condition ready status during async", + old: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + Conditions: []servicecatalog.InstanceCondition{{Status: servicecatalog.ConditionFalse}}, + }, + new: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + Conditions: []servicecatalog.InstanceCondition{{Status: servicecatalog.ConditionTrue}}, + }, + valid: true, + err: "", + }, + { + name: "Update instance condition ready status during async false", + old: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + Conditions: []servicecatalog.InstanceCondition{{Status: servicecatalog.ConditionFalse}}, + }, + new: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + Conditions: []servicecatalog.InstanceCondition{{Status: servicecatalog.ConditionTrue}}, + }, + valid: true, + err: "", + }, + { + name: "Update instance condition to ready status and finish async op", + old: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: true, + Conditions: []servicecatalog.InstanceCondition{{Status: servicecatalog.ConditionFalse}}, + }, + new: &servicecatalog.InstanceStatus{ + AsyncOpInProgress: false, + Conditions: []servicecatalog.InstanceCondition{{Status: servicecatalog.ConditionTrue}}, + }, + valid: true, + err: "", + }, + } + + for _, tc := range cases { + old := &servicecatalog.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: servicecatalog.InstanceSpec{ + ServiceClassName: "test-serviceclass", + PlanName: "Test-Plan", + }, + Status: *tc.old, + } + new := &servicecatalog.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: servicecatalog.InstanceSpec{ + ServiceClassName: "test-serviceclass", + PlanName: "Test-Plan", + }, + Status: *tc.new, + } + + errs := ValidateInstanceStatusUpdate(new, old) + if len(errs) != 0 && tc.valid { + t.Errorf("%v: unexpected error: %v", tc.name, errs) + continue + } else if len(errs) == 0 && !tc.valid { + t.Errorf("%v: unexpected success", tc.name) + } + if !tc.valid { + for _, err := range errs { + if !strings.Contains(err.Detail, tc.err) { + t.Errorf("Error %q did not contain expected message %q", err.Detail, tc.err) + } + } + } + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass.go index fba26e1a51dc..f62004623f94 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass.go @@ -81,6 +81,10 @@ func ValidateServiceClass(serviceclass *sc.ServiceClass) field.ErrorList { allErrs = append(allErrs, field.Invalid(field.NewPath("externalID"), serviceclass.ExternalID, msg)) } + if len(serviceclass.Plans) < 1 { + allErrs = append(allErrs, field.Invalid(field.NewPath("plans"), serviceclass.Plans, "at least one plan is required")) + } + planNames := sets.NewString() for i, plan := range serviceclass.Plans { planPath := field.NewPath("plans").Index(i) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass_test.go index 6113fc91ba83..5cfb13343afc 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/validation/serviceclass_test.go @@ -144,6 +144,15 @@ func TestValidateServiceClass(t *testing.T) { }(), valid: false, }, + { + name: "invalid serviceClass - no plans", + serviceClass: func() *servicecatalog.ServiceClass { + s := validServiceClass() + s.Plans = nil + return s + }(), + valid: false, + }, } for _, tc := range cases { diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/zz_generated.deepcopy.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/zz_generated.deepcopy.go index 8e00a8b94ed4..fb73a43fe127 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/zz_generated.deepcopy.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/zz_generated.deepcopy.go @@ -36,12 +36,14 @@ func init() { // to allow building arbitrary schemes. func RegisterDeepCopies(scheme *runtime.Scheme) error { return scheme.AddGeneratedDeepCopyFuncs( + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_AlphaPodPresetTemplate, InType: reflect.TypeOf(&AlphaPodPresetTemplate{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_Binding, InType: reflect.TypeOf(&Binding{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BindingCondition, InType: reflect.TypeOf(&BindingCondition{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BindingList, InType: reflect.TypeOf(&BindingList{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BindingSpec, InType: reflect.TypeOf(&BindingSpec{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BindingStatus, InType: reflect.TypeOf(&BindingStatus{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_Broker, InType: reflect.TypeOf(&Broker{})}, + conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BrokerAuthInfo, InType: reflect.TypeOf(&BrokerAuthInfo{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BrokerCondition, InType: reflect.TypeOf(&BrokerCondition{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BrokerList, InType: reflect.TypeOf(&BrokerList{})}, conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_servicecatalog_BrokerSpec, InType: reflect.TypeOf(&BrokerSpec{})}, @@ -57,6 +59,20 @@ func RegisterDeepCopies(scheme *runtime.Scheme) error { ) } +func DeepCopy_servicecatalog_AlphaPodPresetTemplate(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*AlphaPodPresetTemplate) + out := out.(*AlphaPodPresetTemplate) + *out = *in + if newVal, err := c.DeepCopy(&in.Selector); err != nil { + return err + } else { + out.Selector = *newVal.(*v1.LabelSelector) + } + return nil + } +} + func DeepCopy_servicecatalog_Binding(in interface{}, out interface{}, c *conversion.Cloner) error { { in := in.(*Binding) @@ -118,6 +134,13 @@ func DeepCopy_servicecatalog_BindingSpec(in interface{}, out interface{}, c *con *out = newVal.(*runtime.RawExtension) } } + if in.AlphaPodPresetTemplate != nil { + in, out := &in.AlphaPodPresetTemplate, &out.AlphaPodPresetTemplate + *out = new(AlphaPodPresetTemplate) + if err := DeepCopy_servicecatalog_AlphaPodPresetTemplate(*in, *out, c); err != nil { + return err + } + } return nil } } @@ -165,6 +188,20 @@ func DeepCopy_servicecatalog_Broker(in interface{}, out interface{}, c *conversi } } +func DeepCopy_servicecatalog_BrokerAuthInfo(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*BrokerAuthInfo) + out := out.(*BrokerAuthInfo) + *out = *in + if in.BasicAuthSecret != nil { + in, out := &in.BasicAuthSecret, &out.BasicAuthSecret + *out = new(api_v1.ObjectReference) + **out = **in + } + return nil + } +} + func DeepCopy_servicecatalog_BrokerCondition(in interface{}, out interface{}, c *conversion.Cloner) error { { in := in.(*BrokerCondition) @@ -198,10 +235,12 @@ func DeepCopy_servicecatalog_BrokerSpec(in interface{}, out interface{}, c *conv in := in.(*BrokerSpec) out := out.(*BrokerSpec) *out = *in - if in.AuthSecret != nil { - in, out := &in.AuthSecret, &out.AuthSecret - *out = new(api_v1.ObjectReference) - **out = **in + if in.AuthInfo != nil { + in, out := &in.AuthInfo, &out.AuthInfo + *out = new(BrokerAuthInfo) + if err := DeepCopy_servicecatalog_BrokerAuthInfo(*in, *out, c); err != nil { + return err + } } return nil } @@ -400,6 +439,30 @@ func DeepCopy_servicecatalog_ServicePlan(in interface{}, out interface{}, c *con *out = newVal.(*runtime.RawExtension) } } + if in.AlphaInstanceCreateParameterSchema != nil { + in, out := &in.AlphaInstanceCreateParameterSchema, &out.AlphaInstanceCreateParameterSchema + if newVal, err := c.DeepCopy(*in); err != nil { + return err + } else { + *out = newVal.(*runtime.RawExtension) + } + } + if in.AlphaInstanceUpdateParameterSchema != nil { + in, out := &in.AlphaInstanceUpdateParameterSchema, &out.AlphaInstanceUpdateParameterSchema + if newVal, err := c.DeepCopy(*in); err != nil { + return err + } else { + *out = newVal.(*runtime.RawExtension) + } + } + if in.AlphaBindingCreateParameterSchema != nil { + in, out := &in.AlphaBindingCreateParameterSchema, &out.AlphaBindingCreateParameterSchema + if newVal, err := c.DeepCopy(*in); err != nil { + return err + } else { + *out = newVal.(*runtime.RawExtension) + } + } return nil } } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/bind_request.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/bind_request.go new file mode 100644 index 000000000000..c4d236b113ff --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/bind_request.go @@ -0,0 +1,28 @@ +/* +Copyright 2017 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 server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// BindRequest is the struct to contain details of a bind request +type BindRequest struct { + InstanceID string + BindingID string + Details brokerapi.BindDetails +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/convert_catalog.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/convert_catalog.go new file mode 100644 index 000000000000..4ee84710b0a6 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/convert_catalog.go @@ -0,0 +1,60 @@ +/* +Copyright 2017 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 server + +import ( + pkgbrokerapi "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + pivbrokerapi "github.com/pivotal-cf/brokerapi" +) + +// ConvertCatalog converts a (github.com/kubernetes-incubator/service-catalog/pkg/brokerapi).Catalog +// to an array of brokerapi.Services +func ConvertCatalog(cat *pkgbrokerapi.Catalog) []pivbrokerapi.Service { + ret := make([]pivbrokerapi.Service, len(cat.Services)) + for i, svc := range cat.Services { + ret[i] = convertService(svc) + } + return ret +} + +func convertService(svc *pkgbrokerapi.Service) pivbrokerapi.Service { + return pivbrokerapi.Service{ + ID: svc.ID, + Name: svc.Name, + Description: svc.Description, + Bindable: svc.Bindable, + Tags: svc.Tags, + PlanUpdatable: svc.PlanUpdateable, + Plans: convertPlans(svc.Plans), + // TODO: convert Requires, Metadata, DashboardClient + } +} + +func convertPlans(plans []pkgbrokerapi.ServicePlan) []pivbrokerapi.ServicePlan { + ret := make([]pivbrokerapi.ServicePlan, len(plans)) + for i, plan := range plans { + ret[i] = pivbrokerapi.ServicePlan{ + ID: plan.ID, + Name: plan.Name, + Description: plan.Description, + Free: &plan.Free, + Bindable: plan.Bindable, + // TODO: convert Metadata + } + } + return ret +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/create_func.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/create_func.go new file mode 100644 index 000000000000..582e4c7b9604 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/create_func.go @@ -0,0 +1,33 @@ +/* +Copyright 2017 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 server + +import ( + "net/http/httptest" + + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker" +) + +// NewCreateFunc creates a new brokerapi.CreateFunc according to a broker server running +// in srv +func NewCreateFunc(srv *httptest.Server, user, pass string) brokerapi.CreateFunc { + // type CreateFunc func(name, url, username, password string) BrokerClient + return brokerapi.CreateFunc(func(name, url, username, password string) brokerapi.BrokerClient { + return openservicebroker.NewClient("testclient", srv.URL, user, pass) + }) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/deprovision_request.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/deprovision_request.go new file mode 100644 index 000000000000..e7da462b1182 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/deprovision_request.go @@ -0,0 +1,27 @@ +/* +Copyright 2017 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 server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// DeprovisionRequest is the struct to contain details of a single deprovision request +type DeprovisionRequest struct { + InstanceID string + Details brokerapi.DeprovisionDetails +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/handler.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/handler.go new file mode 100644 index 000000000000..6777d6dfa725 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/handler.go @@ -0,0 +1,133 @@ +/* +Copyright 2017 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 server + +import ( + "context" + + "github.com/pivotal-cf/brokerapi" +) + +// Handler is a fake implementation oif a brokerapi.ServiceBroker. It's useful as a mock +// because it has pre-canned response values for use in testing, and also keeps track of calls +// made to it. Handler is not concurrency-safe +type Handler struct { + Catalog []brokerapi.Service + // Since there are no data passed to catalog calls, this is just the number of calls + // that were made to the catalog endpoint + CatalogRequests int + + ProvisionResp brokerapi.ProvisionedServiceSpec + ProvisionRespError error + ProvisionRequests []ProvisionRequest + + DeprovisionResp brokerapi.DeprovisionServiceSpec + DeprovisonRespErr error + DeprovisionRequests []DeprovisionRequest + + BindResp brokerapi.Binding + BindRespErr error + BindRequests []BindRequest + + UnbindRespErr error + UnbindRequests []UnbindRequest + + UpdateResp brokerapi.UpdateServiceSpec + UpdateRespErr error + UpdateRequests []UpdateRequest + + LastOperationResp brokerapi.LastOperation + LastOperationRespErr error + LastOperationRequests []LastOperationRequest +} + +// NewHandler creates a new fake server handler +func NewHandler() *Handler { + return &Handler{} +} + +// Services increments h.CatalogRequests and returns h.Catalog +func (h *Handler) Services(ctx context.Context) []brokerapi.Service { + h.CatalogRequests++ + return h.Catalog +} + +// Provision adds an element to h.ProvisionRequests and returns +// h.ProvisionResp, h.ProvisionRespError +func (h *Handler) Provision( + ctx context.Context, + instanceID string, + details brokerapi.ProvisionDetails, + asyncAllowed bool, +) (brokerapi.ProvisionedServiceSpec, error) { + h.ProvisionRequests = append(h.ProvisionRequests, ProvisionRequest{ + InstanceID: instanceID, + Details: details, + AsyncAllowed: asyncAllowed, + }) + return h.ProvisionResp, h.ProvisionRespError +} + +// Deprovision adds an element to h.DeprovisionRequests and returns +// h.DeprovisionResp, h.DeprovisionRespErr +func (h *Handler) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + h.DeprovisionRequests = append(h.DeprovisionRequests, DeprovisionRequest{ + InstanceID: instanceID, + Details: details, + }) + return h.DeprovisionResp, h.DeprovisonRespErr +} + +// Bind adds an element to h.BindRequqests and returns h.BindResp, h.BindRespErr +func (h *Handler) Bind(context context.Context, instanceID, bindingID string, details brokerapi.BindDetails) (brokerapi.Binding, error) { + h.BindRequests = append(h.BindRequests, BindRequest{ + InstanceID: instanceID, + BindingID: bindingID, + Details: details, + }) + return h.BindResp, h.BindRespErr +} + +// Unbind adds an element to h.UnbindRequests and returns h.UnbindRespErr +func (h *Handler) Unbind(context context.Context, instanceID, bindingID string, details brokerapi.UnbindDetails) error { + h.UnbindRequests = append(h.UnbindRequests, UnbindRequest{ + InstanceID: instanceID, + BindingID: bindingID, + Details: details, + }) + return h.UnbindRespErr +} + +// Update adds an element to h.UpdateRequests and returns h.UpdateResp, h.UpdateRespErr +func (h *Handler) Update(context context.Context, instanceID string, details brokerapi.UpdateDetails, asyncAllowed bool) (brokerapi.UpdateServiceSpec, error) { + h.UpdateRequests = append(h.UpdateRequests, UpdateRequest{ + InstanceID: instanceID, + Details: details, + AsyncAllowed: asyncAllowed, + }) + return h.UpdateResp, h.UpdateRespErr +} + +// LastOperation adds an element to h.LastOperationRequests and returns +// h.LastOperationResp, h.LastOperationRespErr +func (h *Handler) LastOperation(context context.Context, instanceID, operationData string) (brokerapi.LastOperation, error) { + h.LastOperationRequests = append(h.LastOperationRequests, LastOperationRequest{ + InstanceID: instanceID, + OperationData: operationData, + }) + return h.LastOperationResp, h.LastOperationRespErr +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/init.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/init.go new file mode 100644 index 000000000000..f174e97846ee --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/init.go @@ -0,0 +1,25 @@ +/* +Copyright 2017 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 server + +import ( + "code.cloudfoundry.org/lager" +) + +var ( + logger = lager.NewLogger("server") +) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/last_operation_request.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/last_operation_request.go new file mode 100644 index 000000000000..d5c2545a8fdd --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/last_operation_request.go @@ -0,0 +1,24 @@ +/* +Copyright 2017 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 server + +// LastOperationRequest is the struct that contains details of a single request to get the last +// operation of an ongoing broker action +type LastOperationRequest struct { + InstanceID string + OperationData string +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/provision_request.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/provision_request.go new file mode 100644 index 000000000000..b7f546ccfcda --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/provision_request.go @@ -0,0 +1,28 @@ +/* +Copyright 2017 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 server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// ProvisionRequest is the struct to house details of a single provision request +type ProvisionRequest struct { + InstanceID string + Details brokerapi.ProvisionDetails + AsyncAllowed bool +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/server.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/server.go new file mode 100644 index 000000000000..3d425d4e3e4c --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/server.go @@ -0,0 +1,34 @@ +/* +Copyright 2017 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 server + +import ( + "net/http/httptest" + + "github.com/pivotal-cf/brokerapi" +) + +// Run runs a new test server from the given broker handler and auth credentials +func Run(hdl *Handler, username, password string) *httptest.Server { + httpHandler := brokerapi.New(hdl, logger, brokerapi.BrokerCredentials{ + Username: username, + Password: password, + }) + + srv := httptest.NewServer(httpHandler) + return srv +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/unbind_request.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/unbind_request.go new file mode 100644 index 000000000000..b5780e0faf36 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/unbind_request.go @@ -0,0 +1,28 @@ +/* +Copyright 2017 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 server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// UnbindRequest is the struct to house details of a single unbind request +type UnbindRequest struct { + InstanceID string + BindingID string + Details brokerapi.UnbindDetails +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/update_request.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/update_request.go new file mode 100644 index 000000000000..ea7919b38651 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server/update_request.go @@ -0,0 +1,28 @@ +/* +Copyright 2017 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 server + +import ( + "github.com/pivotal-cf/brokerapi" +) + +// UpdateRequest is the struct that contains details of a single update request +type UpdateRequest struct { + InstanceID string + Details brokerapi.UpdateDetails + AsyncAllowed bool +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client.go index 9f69575cc855..7f8e48c77a57 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client.go @@ -35,15 +35,10 @@ import ( ) const ( - catalogFormatString = "%s/v2/catalog" - serviceInstanceFormatString = "%s/v2/service_instances/%s" - serviceInstanceAsyncFormatString = "%s/v2/service_instances/%s?accepts_incomplete=true" - serviceInstanceDeleteFormatString = "%s/v2/service_instances/%s?service_id=%s&plan_id=%s" - serviceInstanceDeleteAsyncFormatString = "%s/v2/service_instances/%s?service_id=%s&plan_id=%s&accepts_incomplete=true" - pollingFormatString = "%s/v2/service_instances/%s/last_operation?%s" - bindingFormatString = "%s/v2/service_instances/%s/service_bindings/%s" - bindingDeleteFormatString = "%s/v2/service_instances/%s/service_bindings/%s?service_id=%s&plan_id=%s" - queryParamFormatString = "%s=%s" + catalogFormatString = "%s/v2/catalog" + serviceInstanceFormatString = "%s/v2/service_instances/%s" + pollingFormatString = "%s/v2/service_instances/%s/last_operation" + bindingFormatString = "%s/v2/service_instances/%s/service_bindings/%s" httpTimeoutSeconds = 15 pollingIntervalSeconds = 1 @@ -118,7 +113,7 @@ func NewClient(name, url, username, password string) brokerapi.BrokerClient { func (c *openServiceBrokerClient) GetCatalog() (*brokerapi.Catalog, error) { catalogURL := fmt.Sprintf(catalogFormatString, c.url) - req, err := c.newOSBRequest(http.MethodGet, catalogURL, nil) + req, err := c.newOSBRequest(http.MethodGet, catalogURL, nil, nil) if err != nil { return nil, err } @@ -140,19 +135,24 @@ func (c *openServiceBrokerClient) GetCatalog() (*brokerapi.Catalog, error) { } func (c *openServiceBrokerClient) CreateServiceInstance(ID string, req *brokerapi.CreateServiceInstanceRequest) (*brokerapi.CreateServiceInstanceResponse, int, error) { - var serviceInstanceURL string - - if req.AcceptsIncomplete { - serviceInstanceURL = fmt.Sprintf(serviceInstanceAsyncFormatString, c.url, ID) - } else { - serviceInstanceURL = fmt.Sprintf(serviceInstanceFormatString, c.url, ID) - } - - // TODO: Handle the auth - resp, err := sendOSBRequest(c, http.MethodPut, serviceInstanceURL, req) + serviceInstanceURL := fmt.Sprintf(serviceInstanceFormatString, c.url, ID) + + resp, err := sendOSBRequest( + c, + http.MethodPut, + serviceInstanceURL, + map[string]string{ + "accepts_incomplete": fmt.Sprintf("%t", req.AcceptsIncomplete), + }, + req, + ) if err != nil { glog.Errorf("Error sending create service instance request to broker %q at %v: response: %v error: %#v", c.name, serviceInstanceURL, resp, err) - return nil, resp.StatusCode, errRequest{message: err.Error()} + errReq := errRequest{message: err.Error()} + if resp == nil { + return nil, 0, errReq + } + return nil, resp.StatusCode, errReq } defer resp.Body.Close() @@ -185,16 +185,19 @@ func (c *openServiceBrokerClient) UpdateServiceInstance(ID string, req *brokerap } func (c *openServiceBrokerClient) DeleteServiceInstance(ID string, req *brokerapi.DeleteServiceInstanceRequest) (*brokerapi.DeleteServiceInstanceResponse, int, error) { - var serviceInstanceURL string - - if req.AcceptsIncomplete { - serviceInstanceURL = fmt.Sprintf(serviceInstanceDeleteAsyncFormatString, c.url, ID, req.ServiceID, req.PlanID) - } else { - serviceInstanceURL = fmt.Sprintf(serviceInstanceDeleteFormatString, c.url, ID, req.ServiceID, req.PlanID) - } - - // TODO: Handle the auth - resp, err := sendOSBRequest(c, http.MethodDelete, serviceInstanceURL, req) + serviceInstanceURL := fmt.Sprintf(serviceInstanceFormatString, c.url, ID) + + resp, err := sendOSBRequest( + c, + http.MethodDelete, + serviceInstanceURL, + map[string]string{ + "service_id": req.ServiceID, + "plan_id": req.PlanID, + "accepts_incomplete": fmt.Sprintf("%t", req.AcceptsIncomplete), + }, + req, + ) if err != nil { glog.Errorf("Error sending delete service instance request to broker %q at %v: response: %v error: %#v", c.name, serviceInstanceURL, resp, err) return nil, resp.StatusCode, errRequest{message: err.Error()} @@ -231,8 +234,12 @@ func (c *openServiceBrokerClient) CreateServiceBinding(instanceID, bindingID str serviceBindingURL := fmt.Sprintf(bindingFormatString, c.url, instanceID, bindingID) - // TODO: Handle the auth - createHTTPReq, err := c.newOSBRequest("PUT", serviceBindingURL, bytes.NewReader(jsonBytes)) + createHTTPReq, err := c.newOSBRequest( + http.MethodPut, + serviceBindingURL, + nil, + bytes.NewReader(jsonBytes), + ) if err != nil { return nil, err } @@ -264,10 +271,17 @@ func (c *openServiceBrokerClient) CreateServiceBinding(instanceID, bindingID str } func (c *openServiceBrokerClient) DeleteServiceBinding(instanceID, bindingID, serviceID, planID string) error { - serviceBindingURL := fmt.Sprintf(bindingDeleteFormatString, c.url, instanceID, bindingID, serviceID, planID) + serviceBindingURL := fmt.Sprintf(bindingFormatString, c.url, instanceID, bindingID) - // TODO: Handle the auth - deleteHTTPReq, err := c.newOSBRequest("DELETE", serviceBindingURL, nil) + deleteHTTPReq, err := c.newOSBRequest( + http.MethodDelete, + serviceBindingURL, + map[string]string{ + "service_id": serviceID, + "plan_id": planID, + }, + nil, + ) if err != nil { glog.Errorf("Failed to create new HTTP request: %v", err) return err @@ -293,14 +307,24 @@ func (c *openServiceBrokerClient) DeleteServiceBinding(instanceID, bindingID, se } func (c *openServiceBrokerClient) PollServiceInstance(ID string, req *brokerapi.LastOperationRequest) (*brokerapi.LastOperationResponse, int, error) { - q, err := createPollParameters(req) - if err != nil { - glog.Errorf("Failed to create query parameters for poll last operation: %v", err) - return nil, 0, err + if req.ServiceID == "" { + return nil, 0, fmt.Errorf("LastOperationRequest is missing service_id") } - url := fmt.Sprintf(pollingFormatString, c.url, ID, q) - pollReq := brokerapi.LastOperationRequest{} - resp, err := sendOSBRequest(c, http.MethodGet, url, pollReq) + if req.PlanID == "" { + return nil, 0, fmt.Errorf("LastOperationRequest is missing plan_id") + } + url := fmt.Sprintf(pollingFormatString, c.url, ID) + resp, err := sendOSBRequest( + c, + http.MethodGet, + url, + map[string]string{ + "service_id": req.ServiceID, + "plan_id": req.PlanID, + "operation": req.Operation, + }, + nil, + ) if err != nil { glog.Errorf("Failed to create new HTTP request: %v", err) return nil, 0, err @@ -318,58 +342,21 @@ func (c *openServiceBrokerClient) PollServiceInstance(ID string, req *brokerapi. return &lo, resp.StatusCode, nil } -// createPollParameters creates the query parameter string from the LastOperationRequest -// According to the spec, ServiceID and PlanID should be included, so fail requests -// without them as it indicates programming error on our part. -func createPollParameters(req *brokerapi.LastOperationRequest) (string, error) { - if req.ServiceID == "" { - return "", fmt.Errorf("LastOperationRequest is missing service_id") - } - if req.PlanID == "" { - return "", fmt.Errorf("LastOperationRequest is missing plan_id") - } - - var buffer bytes.Buffer - err := appendQueryParam(&buffer, "service_id", req.ServiceID) - if err != nil { - return "", err - } - err = appendQueryParam(&buffer, "plan_id", req.PlanID) - if err != nil { - return "", err - } - err = appendQueryParam(&buffer, "operation", req.Operation) - if err != nil { - return "", err - } - return buffer.String(), nil -} - -// appendQueryParam appends key=value to buffer if value is non-null. -// If buffer is non-empty appends &key=value -func appendQueryParam(buffer *bytes.Buffer, key, value string) error { - if value == "" { - return nil - } - if buffer.Len() > 0 { - _, err := buffer.WriteString("&") - if err != nil { - return err - } - } - _, err := buffer.WriteString(fmt.Sprintf(queryParamFormatString, key, value)) - return err -} - // SendRequest will serialize 'object' and send it using the given method to // the given URL, through the provided client -func sendOSBRequest(c *openServiceBrokerClient, method string, url string, object interface{}) (*http.Response, error) { +func sendOSBRequest( + c *openServiceBrokerClient, + method string, + url string, + queryParams map[string]string, + object interface{}, +) (*http.Response, error) { data, err := json.Marshal(object) if err != nil { return nil, fmt.Errorf("Failed to marshal request: %s", err.Error()) } - req, err := c.newOSBRequest(method, url, bytes.NewReader(data)) + req, err := c.newOSBRequest(method, url, queryParams, bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("Failed to create request object: %s", err.Error()) } @@ -382,7 +369,12 @@ func sendOSBRequest(c *openServiceBrokerClient, method string, url string, objec return resp, nil } -func (c *openServiceBrokerClient) newOSBRequest(method, urlStr string, body io.Reader) (*http.Request, error) { +func (c *openServiceBrokerClient) newOSBRequest( + method string, + urlStr string, + queryParams map[string]string, + body io.Reader, +) (*http.Request, error) { req, err := http.NewRequest(method, urlStr, body) if err != nil { return nil, err @@ -392,5 +384,12 @@ func (c *openServiceBrokerClient) newOSBRequest(method, urlStr string, body io.R } req.Header.Add(constants.APIVersionHeader, constants.APIVersion) req.SetBasicAuth(c.username, c.password) + if queryParams != nil { + q := req.URL.Query() + for k, v := range queryParams { + q.Set(k, v) + } + req.URL.RawQuery = q.Encode() + } return req, nil } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client_test.go index 21cb514d66f7..ac94b2f13ca9 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/open_service_broker_client_test.go @@ -358,51 +358,19 @@ func TestUnbindGone(t *testing.T) { verifyBindingMethodAndPath(http.MethodDelete, testServiceInstanceID, testServiceBindingID, fbs.Request, t) } -func TestCreatePollParametersMissingServiceID(t *testing.T) { +func TestPollServiceInstanceWithMissingServiceID(t *testing.T) { + fbs, fakeBroker := setup() + defer fbs.Stop() + + c := NewClient(testBrokerName, fakeBroker.Spec.URL, "", "") r := &brokerapi.LastOperationRequest{PlanID: testPlanID} - _, err := createPollParameters(r) + _, _, err := c.PollServiceInstance(testServiceInstanceID, r) if err == nil { - t.Fatalf("createPollParameters did not fail with missing ServiceID") + t.Fatal("PollServiceInstance did not fail with invalid LastOperationRequest") } if !strings.Contains(err.Error(), "missing service_id") { t.Fatalf("Did not find the expected error message 'missing service_id' in error: %s", err) } - -} - -func TestCreatePollParametersMissingPlanID(t *testing.T) { - r := &brokerapi.LastOperationRequest{ServiceID: testServiceID} - _, err := createPollParameters(r) - if err == nil { - t.Fatalf("createPollParameters did not fail with missing PlanID") - } - if !strings.Contains(err.Error(), "missing plan_id") { - t.Fatalf("Did not find the expected error message 'missing plan_id' in error: %s", err) - } -} - -func TestCreatePollParametersNoOperation(t *testing.T) { - r := &brokerapi.LastOperationRequest{ServiceID: testServiceID, PlanID: testPlanID} - q, err := createPollParameters(r) - if err != nil { - t.Fatalf("createPollParameters failed when expected to succeed: %s", err) - } - exp := "service_id=" + testServiceID + "&plan_id=" + testPlanID - if q != exp { - t.Fatalf("expected query parameters %q got %q\n", exp, q) - } -} - -func TestCreatePollParametersWithOperation(t *testing.T) { - r := &brokerapi.LastOperationRequest{ServiceID: testServiceID, PlanID: testPlanID, Operation: testOperation} - q, err := createPollParameters(r) - if err != nil { - t.Fatalf("createPollParameters failed when expected to succeed: %s", err) - } - exp := "service_id=" + testServiceID + "&plan_id=" + testPlanID + "&operation=" + testOperation - if q != exp { - t.Fatalf("expected query parameters %q got %q\n", exp, q) - } } func TestPollServiceInstanceWithMissingPlanID(t *testing.T) { diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/util/fake_broker_server.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/util/fake_broker_server.go index 9869856edf92..4e153b056b7c 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/util/fake_broker_server.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/openservicebroker/util/fake_broker_server.go @@ -143,11 +143,15 @@ func (f *FakeBrokerServer) provisionHandler(w http.ResponseWriter, r *http.Reque func (f *FakeBrokerServer) deprovisionHandler(w http.ResponseWriter, r *http.Request) { glog.Info("fake deprovision called") f.Request = r - req := &brokerapi.DeleteServiceInstanceRequest{} - if err := util.BodyToObject(r, req); err != nil { - w.WriteHeader(http.StatusBadRequest) - return + req := &brokerapi.DeleteServiceInstanceRequest{ + ServiceID: r.URL.Query().Get("service_id"), + PlanID: r.URL.Query().Get("plan_id"), } + incompleteStr := r.URL.Query().Get("accepts_incomplete") + if incompleteStr == "true" { + req.AcceptsIncomplete = true + } + f.RequestObject = req if r.FormValue(asyncProvisionQueryParamKey) != "true" { diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/schemas.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/schemas.go index d4098785ba1e..df1746d80b14 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/schemas.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/schemas.go @@ -16,16 +16,28 @@ limitations under the License. package brokerapi -// Schemas represents a broker's schemas for both service instances and service -// bindings +// Schemas represents a plan's schemas for service instance and binding create +// and update. type Schemas struct { - Instance Schema `json:"instance"` - Binding Schema `json:"binding"` + ServiceInstances *ServiceInstanceSchema `json:"service_instance,omitempty"` + ServiceBindings *ServiceBindingSchema `json:"service_binding,omitempty"` } -// Schema consists of the schema for inputs and the schema for outputs. -// Schemas are in the form of JSON Schema v4 (http://json-schema.org/). -type Schema struct { - Inputs string `json:"inputs"` - Outputs string `json:"outputs"` +// ServiceInstanceSchema represents a plan's schemas for a create and update +// of a service instance. +type ServiceInstanceSchema struct { + Create *InputParameters `json:"create,omitempty"` + Update *InputParameters `json:"update,omitempty"` +} + +// ServiceBindingSchema represents a plan's schemas for the parameters +// accepted for binding creation. +type ServiceBindingSchema struct { + Create *InputParameters `json:"create,omitempty"` +} + +// InputParameters represents a schema for input parameters for creation or +// update of an API resource. +type InputParameters struct { + Parameters interface{} `json:"parameters,omitempty"` } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/service_plan.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/service_plan.go index cc8fb3ef4d82..8eafb9e741b7 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/service_plan.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/service_plan.go @@ -24,6 +24,6 @@ type ServicePlan struct { Description string `json:"description"` Metadata interface{} `json:"metadata,omitempty"` Free bool `json:"free,omitempty"` - Schemas *Schemas `json:"schemas,omitempty"` Bindable *bool `json:"bindable,omitempty"` + Schemas *Schemas `json:"schemas,omitempty"` } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller.go index 17e38860d08e..d4b5b6c40138 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller.go @@ -19,27 +19,22 @@ package controller import ( "encoding/json" "fmt" - "net/http" "time" "github.com/ghodss/yaml" "github.com/golang/glog" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" runtimeutil "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" "k8s.io/client-go/pkg/api" - "k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" - checksum "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/checksum/versioned/v1alpha1" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" servicecatalogclientset "github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/clientset/typed/servicecatalog/v1alpha1" @@ -210,1321 +205,6 @@ func worker(queue workqueue.RateLimitingInterface, resourceType string, maxRetri } } -// Broker handlers and control-loop - -func (c *controller) brokerAdd(obj interface{}) { - // DeletionHandlingMetaNamespaceKeyFunc returns a unique key for the resource and - // handles the special case where the resource is of DeletedFinalStateUnknown type, which - // acts a place holder for resources that have been deleted from storage but the watch event - // confirming the deletion has not yet arrived. - // Generally, the key is "namespace/name" for namespaced-scoped resources and - // just "name" for cluster scoped resources. - key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) - if err != nil { - glog.Errorf("Couldn't get key for object %+v: %v", obj, err) - return - } - c.brokerQueue.Add(key) -} - -func (c *controller) brokerUpdate(oldObj, newObj interface{}) { - c.brokerAdd(newObj) -} - -func (c *controller) brokerDelete(obj interface{}) { - broker, ok := obj.(*v1alpha1.Broker) - if broker == nil || !ok { - return - } - - glog.V(4).Infof("Received delete event for Broker %v", broker.Name) -} - -// the Message strings have a terminating period and space so they can -// be easily combined with a follow on specific message. -const ( - errorFetchingCatalogReason string = "ErrorFetchingCatalog" - errorFetchingCatalogMessage string = "Error fetching catalog. " - errorSyncingCatalogReason string = "ErrorSyncingCatalog" - errorSyncingCatalogMessage string = "Error syncing catalog from Broker. " - errorWithParameters string = "ErrorWithParameters" - errorListingServiceClassesReason string = "ErrorListingServiceClasses" - errorListingServiceClassesMessage string = "Error listing service classes." - errorDeletingServiceClassReason string = "ErrorDeletingServiceClass" - errorDeletingServiceClassMessage string = "Error deleting service class." - errorNonexistentServiceClassReason string = "ReferencesNonexistentServiceClass" - errorNonexistentServiceClassMessage string = "ReferencesNonexistentServiceClass" - errorNonexistentServicePlanReason string = "ReferencesNonexistentServicePlan" - errorNonexistentBrokerReason string = "ReferencesNonexistentBroker" - errorNonexistentInstanceReason string = "ReferencesNonexistentInstance" - errorAuthCredentialsReason string = "ErrorGettingAuthCredentials" - errorFindingNamespaceInstanceReason string = "ErrorFindingNamespaceForInstance" - errorProvisionCalledReason string = "ProvisionCallFailed" - errorDeprovisionCalledReason string = "DeprovisionCallFailed" - errorBindCallReason string = "BindCallFailed" - errorInjectingBindResultReason string = "ErrorInjectingBindResult" - errorEjectingBindReason string = "ErrorEjectingBinding" - errorEjectingBindMessage string = "Error ejecting binding." - errorUnbindCallReason string = "UnbindCallFailed" - errorWithOngoingAsyncOperation string = "ErrorAsyncOperationInProgress" - errorWithOngoingAsyncOperationMessage string = "Another operation for this service instance is in progress. " - errorNonbindableServiceClassReason string = "ErrorNonbindableServiceClass" - errorInstanceNotReadyReason string = "ErrorInstanceNotReady" - - successInjectedBindResultReason string = "InjectedBindResult" - successInjectedBindResultMessage string = "Injected bind result" - successDeprovisionReason string = "DeprovisionedSuccessfully" - successDeprovisionMessage string = "The instance was deprovisioned successfully" - successProvisionReason string = "ProvisionedSuccessfully" - successProvisionMessage string = "The instance was provisioned successfully" - successFetchedCatalogReason string = "FetchedCatalog" - successFetchedCatalogMessage string = "Successfully fetched catalog entries from broker." - successBrokerDeletedReason string = "DeletedSuccessfully" - successBrokerDeletedMessage string = "The broker %v was deleted successfully." - successUnboundReason string = "UnboundSuccessfully" - asyncProvisioningReason string = "Provisioning" - asyncProvisioningMessage string = "The instance is being provisioned asynchronously" - asyncDeprovisioningReason string = "Derovisioning" - asyncDeprovisioningMessage string = "The instance is being deprovisioned asynchronously" -) - -// shouldReconcileBroker determines whether a broker should be reconciled; it -// returns true unless the broker has a ready condition with status true and -// the controller's broker relist interval has not elapsed since the broker's -// ready condition became true. -func shouldReconcileBroker(broker *v1alpha1.Broker, now time.Time, relistInterval time.Duration) bool { - if broker.DeletionTimestamp != nil || len(broker.Status.Conditions) == 0 { - // If the deletion timestamp is set or the broker has no status - // conditions, we should reconcile it. - return true - } - - // find the ready condition in the broker's status - for _, condition := range broker.Status.Conditions { - if condition.Type == v1alpha1.BrokerConditionReady { - // The broker has a ready condition - - if condition.Status == v1alpha1.ConditionTrue { - // The broker's ready condition has status true, meaning that - // at some point, we successfully listed the broker's catalog. - // We should reconcile the broker (relist the broker's - // catalog) if it has been longer than the configured relist - // interval since the broker's ready condition became true. - return now.After(condition.LastTransitionTime.Add(relistInterval)) - } - - // The broker's ready condition wasn't true; we should try to re- - // list the broker. - return true - } - } - - // The broker didn't have a ready condition; we should reconcile it. - return true -} - -func (c *controller) reconcileBrokerKey(key string) error { - broker, err := c.brokerLister.Get(key) - if errors.IsNotFound(err) { - glog.Infof("Not doing work for Broker %v because it has been deleted", key) - return nil - } - if err != nil { - glog.Infof("Unable to retrieve Broker %v from store: %v", key, err) - return err - } - - return c.reconcileBroker(broker) -} - -// reconcileBroker is the control-loop that reconciles a Broker. -func (c *controller) reconcileBroker(broker *v1alpha1.Broker) error { - glog.V(4).Infof("Processing Broker %v", broker.Name) - - // If the broker's ready condition is true and the relist interval has not - // elapsed, do not reconcile it. - if !shouldReconcileBroker(broker, time.Now(), c.brokerRelistInterval) { - glog.V(10).Infof("Not processing Broker %v because relist interval has not elapsed since the broker became ready", broker.Name) - return nil - } - - username, password, err := getAuthCredentialsFromBroker(c.kubeClient, broker) - if err != nil { - s := fmt.Sprintf("Error getting broker auth credentials for broker %q: %s", broker.Name, err) - glog.Info(s) - c.recorder.Event(broker, api.EventTypeWarning, errorAuthCredentialsReason, s) - c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorFetchingCatalogReason, errorFetchingCatalogMessage+s) - return err - } - - glog.V(4).Infof("Creating client for Broker %v, URL: %v", broker.Name, broker.Spec.URL) - brokerClient := c.brokerClientCreateFunc(broker.Name, broker.Spec.URL, username, password) - - if broker.DeletionTimestamp == nil { // Add or update - glog.V(4).Infof("Adding/Updating Broker %v", broker.Name) - brokerCatalog, err := brokerClient.GetCatalog() - if err != nil { - s := fmt.Sprintf("Error getting broker catalog for broker %q: %s", broker.Name, err) - glog.Warning(s) - c.recorder.Eventf(broker, api.EventTypeWarning, errorFetchingCatalogReason, s) - c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorFetchingCatalogReason, - errorFetchingCatalogMessage+s) - return err - } - glog.V(5).Infof("Successfully fetched %v catalog entries for Broker %v", len(brokerCatalog.Services), broker.Name) - - glog.V(4).Infof("Converting catalog response for Broker %v into service-catalog API", broker.Name) - catalog, err := convertCatalog(brokerCatalog) - if err != nil { - s := fmt.Sprintf("Error converting catalog payload for broker %q to service-catalog API: %s", broker.Name, err) - glog.Warning(s) - c.recorder.Eventf(broker, api.EventTypeWarning, errorSyncingCatalogReason, s) - c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorSyncingCatalogReason, errorSyncingCatalogMessage+s) - return err - } - glog.V(5).Infof("Successfully converted catalog payload from Broker %v to service-catalog API", broker.Name) - - for _, serviceClass := range catalog { - glog.V(4).Infof("Reconciling serviceClass %v (broker %v)", serviceClass.Name, broker.Name) - if err := c.reconcileServiceClassFromBrokerCatalog(broker, serviceClass); err != nil { - s := fmt.Sprintf("Error reconciling serviceClass %q (broker %q): %s", serviceClass.Name, broker.Name, err) - glog.Warning(s) - c.recorder.Eventf(broker, api.EventTypeWarning, errorSyncingCatalogReason, s) - c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorSyncingCatalogReason, - errorSyncingCatalogMessage+s) - return err - } - - glog.V(5).Infof("Reconciled serviceClass %v (broker %v)", serviceClass.Name, broker.Name) - } - - c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionTrue, successFetchedCatalogReason, successFetchedCatalogMessage) - c.recorder.Event(broker, api.EventTypeNormal, successFetchedCatalogReason, successFetchedCatalogMessage) - return nil - } - - // All updates not having a DeletingTimestamp will have been handled above - // and returned early. If we reach this point, we're dealing with an update - // that's actually a soft delete-- i.e. we have some finalization to do. - // Since the potential exists for a broker to have multiple finalizers and - // since those most be cleared in order, we proceed with the soft delete - // only if it's "our turn--" i.e. only if the finalizer we care about is at - // the head of the finalizers list. - // TODO: Should we use a more specific string here? - if len(broker.Finalizers) > 0 && broker.Finalizers[0] == "kubernetes" { - glog.V(4).Infof("Finalizing Broker %v", broker.Name) - - // Get ALL ServiceClasses. Remove those that reference this Broker. - svcClasses, err := c.serviceClassLister.List(labels.Everything()) - if err != nil { - c.updateBrokerCondition( - broker, - v1alpha1.BrokerConditionReady, - v1alpha1.ConditionUnknown, - errorListingServiceClassesReason, - errorListingServiceClassesMessage, - ) - c.recorder.Eventf(broker, api.EventTypeWarning, errorListingServiceClassesReason, "%v %v", errorListingServiceClassesMessage, err) - return err - } - - // Delete ServiceClasses that are for THIS Broker. - for _, svcClass := range svcClasses { - if svcClass.BrokerName == broker.Name { - err := c.serviceCatalogClient.ServiceClasses().Delete(svcClass.Name, &metav1.DeleteOptions{}) - if err != nil && !errors.IsNotFound(err) { - s := fmt.Sprintf("Error deleting ServiceClass %q (Broker %q): %s", svcClass.Name, broker.Name, err) - glog.Warning(s) - c.updateBrokerCondition( - broker, - v1alpha1.BrokerConditionReady, - v1alpha1.ConditionUnknown, - errorDeletingServiceClassMessage, - errorDeletingServiceClassReason+s, - ) - c.recorder.Eventf(broker, api.EventTypeWarning, errorDeletingServiceClassReason, "%v %v", errorDeletingServiceClassMessage, s) - return err - } - } - } - - c.updateBrokerCondition( - broker, - v1alpha1.BrokerConditionReady, - v1alpha1.ConditionFalse, - successBrokerDeletedReason, - "The broker was deleted successfully", - ) - // Clear the finalizer - c.updateBrokerFinalizers(broker, broker.Finalizers[1:]) - - c.recorder.Eventf(broker, api.EventTypeNormal, successBrokerDeletedReason, successBrokerDeletedMessage, broker.Name) - glog.V(5).Infof("Successfully deleted Broker %v", broker.Name) - return nil - } - - return nil -} - -// reconcileServiceClassFromBrokerCatalog reconciles a ServiceClass after the -// Broker's catalog has been re-listed. -func (c *controller) reconcileServiceClassFromBrokerCatalog(broker *v1alpha1.Broker, serviceClass *v1alpha1.ServiceClass) error { - serviceClass.BrokerName = broker.Name - - existingServiceClass, err := c.serviceClassLister.Get(serviceClass.Name) - if errors.IsNotFound(err) { - // An error returned from a lister Get call means that the object does - // not exist. Create a new ServiceClass. - if _, err := c.serviceCatalogClient.ServiceClasses().Create(serviceClass); err != nil { - glog.Errorf("Error creating serviceClass %v from Broker %v: %v", serviceClass.Name, broker.Name, err) - return err - } - - return nil - } else if err != nil { - glog.Errorf("Error getting serviceClass %v: %v", serviceClass.Name, err) - return err - } - - if existingServiceClass.BrokerName != broker.Name { - errMsg := fmt.Sprintf("ServiceClass %q for Broker %q already exists for Broker %q", serviceClass.Name, broker.Name, existingServiceClass.BrokerName) - glog.Error(errMsg) - return fmt.Errorf(errMsg) - } - - if existingServiceClass.ExternalID != serviceClass.ExternalID { - errMsg := fmt.Sprintf("ServiceClass %q already exists with OSB guid %q, received different guid %q", serviceClass.Name, existingServiceClass.ExternalID, serviceClass.ExternalID) - glog.Error(errMsg) - return fmt.Errorf(errMsg) - } - - glog.V(5).Infof("Found existing serviceClass %v; updating", serviceClass.Name) - - // There was an existing service class -- project the update onto it and - // update it. - clone, err := api.Scheme.DeepCopy(existingServiceClass) - if err != nil { - return err - } - - toUpdate := clone.(*v1alpha1.ServiceClass) - toUpdate.Bindable = serviceClass.Bindable - toUpdate.Plans = serviceClass.Plans - toUpdate.PlanUpdatable = serviceClass.PlanUpdatable - toUpdate.AlphaTags = serviceClass.AlphaTags - toUpdate.Description = serviceClass.Description - toUpdate.AlphaRequires = serviceClass.AlphaRequires - - if _, err := c.serviceCatalogClient.ServiceClasses().Update(toUpdate); err != nil { - glog.Errorf("Error updating serviceClass %v from Broker %v: %v", serviceClass.Name, broker.Name, err) - return err - } - - return nil -} - -// updateBrokerReadyCondition updates the ready condition for the given Broker -// with the given status, reason, and message. -func (c *controller) updateBrokerCondition(broker *v1alpha1.Broker, conditionType v1alpha1.BrokerConditionType, status v1alpha1.ConditionStatus, reason, message string) error { - clone, err := api.Scheme.DeepCopy(broker) - if err != nil { - return err - } - toUpdate := clone.(*v1alpha1.Broker) - newCondition := v1alpha1.BrokerCondition{ - Type: conditionType, - Status: status, - Reason: reason, - Message: message, - } - - t := time.Now() - - if len(broker.Status.Conditions) == 0 { - glog.Infof("Setting lastTransitionTime for Broker %q condition %q to %v", broker.Name, conditionType, t) - newCondition.LastTransitionTime = metav1.NewTime(t) - toUpdate.Status.Conditions = []v1alpha1.BrokerCondition{newCondition} - } else { - for i, cond := range broker.Status.Conditions { - if cond.Type == conditionType { - if cond.Status != newCondition.Status { - glog.Infof("Found status change for Broker %q condition %q: %q -> %q; setting lastTransitionTime to %v", broker.Name, conditionType, cond.Status, status, t) - newCondition.LastTransitionTime = metav1.NewTime(t) - } else { - newCondition.LastTransitionTime = cond.LastTransitionTime - } - - toUpdate.Status.Conditions[i] = newCondition - break - } - } - } - - glog.V(4).Infof("Updating ready condition for Broker %v to %v", broker.Name, status) - _, err = c.serviceCatalogClient.Brokers().UpdateStatus(toUpdate) - if err != nil { - glog.Errorf("Error updating ready condition for Broker %v: %v", broker.Name, err) - } else { - glog.V(5).Infof("Updated ready condition for Broker %v to %v", broker.Name, status) - } - - return err -} - -// updateBrokerFinalizers updates the given finalizers for the given Broker. -func (c *controller) updateBrokerFinalizers( - broker *v1alpha1.Broker, - finalizers []string) error { - - clone, err := api.Scheme.DeepCopy(broker) - if err != nil { - return err - } - toUpdate := clone.(*v1alpha1.Broker) - - toUpdate.Finalizers = finalizers - - logContext := fmt.Sprintf("finalizers for Broker %v to %v", - broker.Name, finalizers) - - glog.V(4).Infof("Updating %v", logContext) - _, err = c.serviceCatalogClient.Brokers().UpdateStatus(toUpdate) - if err != nil { - glog.Errorf("Error updating %v: %v", logContext, err) - } - return err -} - -// Service class handlers and control-loop - -func (c *controller) serviceClassAdd(obj interface{}) { - key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) - if err != nil { - glog.Errorf("Couldn't get key for object %+v: %v", obj, err) - return - } - c.serviceClassQueue.Add(key) -} - -func (c *controller) reconcileServiceClassKey(key string) error { - serviceClass, err := c.serviceClassLister.Get(key) - if errors.IsNotFound(err) { - glog.Infof("Not doing work for ServiceClass %v because it has been deleted", key) - return nil - } - if err != nil { - glog.Errorf("Unable to retrieve ServiceClass %v from store: %v", key, err) - return err - } - - return c.reconcileServiceClass(serviceClass) -} - -func (c *controller) reconcileServiceClass(serviceClass *v1alpha1.ServiceClass) error { - glog.V(4).Infof("Processing ServiceClass %v", serviceClass.Name) - return nil -} - -func (c *controller) serviceClassUpdate(oldObj, newObj interface{}) { - c.serviceClassAdd(newObj) -} - -func (c *controller) serviceClassDelete(obj interface{}) { - serviceClass, ok := obj.(*v1alpha1.ServiceClass) - if serviceClass == nil || !ok { - return - } - - glog.V(4).Infof("Received delete event for ServiceClass %v", serviceClass.Name) -} - -// Instance handlers and control-loop - -func (c *controller) instanceAdd(obj interface{}) { - key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) - if err != nil { - glog.Errorf("Couldn't get key for object %+v: %v", obj, err) - return - } - // TODO(vaikas): If the obj (which really is an Instance right?) has - // AsyncOpInProgress flag set, just add it directly to c.pollingQueue - // here? Why shouldn't we?? - c.instanceQueue.Add(key) -} - -func (c *controller) reconcileInstanceKey(key string) error { - // For namespace-scoped resources, SplitMetaNamespaceKey splits the key - // i.e. "namespace/name" into two separate strings - namespace, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - return err - } - instance, err := c.instanceLister.Instances(namespace).Get(name) - if errors.IsNotFound(err) { - glog.Infof("Not doing work for Instance %v because it has been deleted", key) - return nil - } - if err != nil { - glog.Errorf("Unable to retrieve Instance %v from store: %v", key, err) - return err - } - - return c.reconcileInstance(instance) -} - -func (c *controller) instanceUpdate(oldObj, newObj interface{}) { - c.instanceAdd(newObj) -} - -// reconcileInstance is the control-loop for reconciling Instances. -func (c *controller) reconcileInstance(instance *v1alpha1.Instance) error { - - // If there's no async op in progress, determine whether the checksum - // has been invalidated by a change to the object. If the instance's - // checksum matches the calculated checksum, there is no work to do. - // If there's an async op in progress, we need to keep polling, hence - // do not bail if checksum hasn't changed. - // - // We only do this if the deletion timestamp is nil, because the deletion - // timestamp changes the object's state in a way that we must reconcile, - // but does not affect the checksum. - if !instance.Status.AsyncOpInProgress { - if instance.Status.Checksum != nil && instance.DeletionTimestamp == nil { - instanceChecksum := checksum.InstanceSpecChecksum(instance.Spec) - if instanceChecksum == *instance.Status.Checksum { - glog.V(4).Infof("Not processing event for Instance %v/%v because checksum showed there is no work to do", instance.Namespace, instance.Name) - return nil - } - } - } - - glog.V(4).Infof("Processing Instance %v/%v", instance.Namespace, instance.Name) - - serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBroker(instance) - if err != nil { - return err - } - - if instance.Status.AsyncOpInProgress { - return c.pollInstance(serviceClass, servicePlan, brokerName, brokerClient, instance) - } - - if instance.DeletionTimestamp == nil { // Add or update - glog.V(4).Infof("Adding/Updating Instance %v/%v", instance.Namespace, instance.Name) - - var parameters map[string]interface{} - if instance.Spec.Parameters != nil { - parameters, err = unmarshalParameters(instance.Spec.Parameters.Raw) - if err != nil { - s := fmt.Sprintf("Failed to unmarshal Instance parameters\n%s\n %s", instance.Spec.Parameters, err) - glog.Warning(s) - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - errorWithParameters, - "Error unmarshaling instance parameters. "+s, - ) - c.recorder.Event(instance, api.EventTypeWarning, errorWithParameters, s) - return err - } - } - - ns, err := c.kubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) - if err != nil { - s := fmt.Sprintf("Failed to get namespace %q during instance create: %s", instance.Namespace, err) - glog.Info(s) - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - errorFindingNamespaceInstanceReason, - "Error finding namespace for instance. "+s, - ) - c.recorder.Event(instance, api.EventTypeWarning, errorFindingNamespaceInstanceReason, s) - return err - } - - request := &brokerapi.CreateServiceInstanceRequest{ - ServiceID: serviceClass.ExternalID, - PlanID: servicePlan.ExternalID, - Parameters: parameters, - OrgID: string(ns.UID), - SpaceID: string(ns.UID), - AcceptsIncomplete: true, - } - if c.enableOSBAPIContextProfle { - request.ContextProfile = brokerapi.ContextProfile{ - Platform: brokerapi.ContextProfilePlatformKubernetes, - Namespace: instance.Namespace, - } - } - - glog.V(4).Infof("Provisioning a new Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - response, respCode, err := brokerClient.CreateServiceInstance(instance.Spec.ExternalID, request) - if err != nil { - s := fmt.Sprintf("Error provisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) - glog.Warning(s) - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - errorProvisionCalledReason, - "Provision call failed. "+s) - c.recorder.Event(instance, api.EventTypeWarning, errorProvisionCalledReason, s) - return err - } - - if response.DashboardURL != "" { - instance.Status.DashboardURL = &response.DashboardURL - } - - // Broker can return either a synchronous or asynchronous - // response, if the response is StatusAccepted it's an async - // and we need to add it to the polling queue. Broker can - // optionally return 'Operation' that will then need to be - // passed back to the broker during polling of last_operation. - if respCode == http.StatusAccepted { - glog.V(5).Infof("Received asynchronous provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) - if response.Operation != "" { - instance.Status.LastOperation = &response.Operation - } - - // Tag this instance as having an ongoing async operation so we can enforce - // no other operations against it can start. - instance.Status.AsyncOpInProgress = true - - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - asyncProvisioningReason, - asyncProvisioningMessage, - ) - c.recorder.Eventf(instance, api.EventTypeNormal, asyncProvisioningReason, asyncProvisioningMessage) - - // Actually, start polling this Service Instance by adding it into the polling queue - key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(instance) - if err != nil { - glog.Errorf("Couldn't create a key for object %+v: %v", instance, err) - return fmt.Errorf("Couldn't create a key for object %+v: %v", instance, err) - } - c.pollingQueue.Add(key) - } else { - glog.V(5).Infof("Successfully provisioned Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) - - // TODO: process response - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionTrue, - successProvisionReason, - successProvisionMessage, - ) - c.recorder.Eventf(instance, api.EventTypeNormal, successProvisionReason, successProvisionMessage) - } - return nil - } - - // All updates not having a DeletingTimestamp will have been handled above - // and returned early. If we reach this point, we're dealing with an update - // that's actually a soft delete-- i.e. we have some finalization to do. - // Since the potential exists for an instance to have multiple finalizers and - // since those most be cleared in order, we proceed with the soft delete - // only if it's "our turn--" i.e. only if the finalizer we care about is at - // the head of the finalizers list. - // TODO: Should we use a more specific string here? - if len(instance.Finalizers) > 0 && instance.Finalizers[0] == "kubernetes" { - glog.V(4).Infof("Finalizing Instance %v/%v", instance.Namespace, instance.Name) - - request := &brokerapi.DeleteServiceInstanceRequest{ - ServiceID: serviceClass.ExternalID, - PlanID: servicePlan.ExternalID, - AcceptsIncomplete: true, - } - - glog.V(4).Infof("Deprovisioning Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - response, respCode, err := brokerClient.DeleteServiceInstance(instance.Spec.ExternalID, request) - - if err != nil { - s := fmt.Sprintf("Error deprovisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) - glog.Warning(s) - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionUnknown, - errorDeprovisionCalledReason, - "Deprovision call failed. "+s) - c.recorder.Event(instance, api.EventTypeWarning, errorDeprovisionCalledReason, s) - return err - } - - if respCode == http.StatusAccepted { - glog.V(5).Infof("Received asynchronous de-provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) - if response.Operation != "" { - instance.Status.LastOperation = &response.Operation - } - - // Tag this instance as having an ongoing async operation so we can enforce - // no other operations against it can start. - instance.Status.AsyncOpInProgress = true - - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - asyncDeprovisioningReason, - asyncDeprovisioningMessage, - ) - } else { - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - successDeprovisionReason, - successDeprovisionMessage, - ) - // Clear the finalizer - c.updateInstanceFinalizers(instance, instance.Finalizers[1:]) - c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) - glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - } - } - - return nil -} - -func (c *controller) pollInstanceInternal(instance *v1alpha1.Instance) error { - glog.V(4).Infof("Processing Instance %v/%v", instance.Namespace, instance.Name) - - serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBroker(instance) - if err != nil { - return err - } - return c.pollInstance(serviceClass, servicePlan, brokerName, brokerClient, instance) -} - -func (c *controller) pollInstance(serviceClass *v1alpha1.ServiceClass, servicePlan *v1alpha1.ServicePlan, brokerName string, brokerClient brokerapi.BrokerClient, instance *v1alpha1.Instance) error { - - // There are some conditions that are different if we're - // deleting, this is more readable than checking the - // timestamps in various places. - deleting := false - if instance.DeletionTimestamp != nil { - deleting = true - } - - lastOperationRequest := &brokerapi.LastOperationRequest{ - ServiceID: serviceClass.ExternalID, - PlanID: servicePlan.ExternalID, - } - if instance.Status.LastOperation != nil && *instance.Status.LastOperation != "" { - lastOperationRequest.Operation = *instance.Status.LastOperation - } - resp, rc, err := brokerClient.PollServiceInstance(instance.Spec.ExternalID, lastOperationRequest) - if err != nil { - glog.Warningf("Poll failed for %v/%v : %s", instance.Namespace, instance.Name, err) - return err - } - glog.V(4).Infof("Poll for %v/%v returned %q : %q", instance.Namespace, instance.Name, resp.State, resp.Description) - - // If the operation was for delete and we receive a http.StatusGone, - // this is considered a success as per the spec, so mark as deleted - // and remove any finalizers. - if rc == http.StatusGone && deleting { - instance.Status.AsyncOpInProgress = false - // Clear the finalizer - if len(instance.Finalizers) > 0 && instance.Finalizers[0] == "kubernetes" { - c.updateInstanceFinalizers(instance, instance.Finalizers[1:]) - } - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - successDeprovisionReason, - successDeprovisionMessage, - ) - c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) - glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - return nil - } - - switch resp.State { - case "in progress": - // The way the worker keeps on requeueing is by returning an error, so - // we need to keep on polling. - // TODO(vaikas): Update the instance condition with progress message here? - return fmt.Errorf("last operation not completed (still in progress) for %v/%v", instance.Namespace, instance.Name) - case "succeeded": - // this gets updated as a side effect in both cases below. - instance.Status.AsyncOpInProgress = false - - // If we were asynchronously deleting a Service Instance, finish - // the finalizers. - if deleting { - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionFalse, - successDeprovisionReason, - successDeprovisionMessage, - ) - // Clear the finalizer - if len(instance.Finalizers) > 0 && instance.Finalizers[0] == "kubernetes" { - c.updateInstanceFinalizers(instance, instance.Finalizers[1:]) - } - c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) - glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - } else { - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - v1alpha1.ConditionTrue, - successProvisionReason, - successProvisionMessage, - ) - } - case "failed": - s := fmt.Sprintf("Error deprovisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q: %q", instance.Namespace, instance.Name, serviceClass.Name, brokerName, resp.Description) - instance.Status.AsyncOpInProgress = false - cond := v1alpha1.ConditionFalse - reason := errorProvisionCalledReason - msg := "Provision call failed: " + s - if deleting { - cond = v1alpha1.ConditionUnknown - reason = errorDeprovisionCalledReason - msg = "Deprovision call failed:" + s - } - c.updateInstanceCondition( - instance, - v1alpha1.InstanceConditionReady, - cond, - reason, - msg, - ) - c.recorder.Event(instance, api.EventTypeWarning, errorDeprovisionCalledReason, s) - default: - glog.Warningf("Got invalid state in LastOperationResponse: %q", resp.State) - return fmt.Errorf("Got invalid state in LastOperationResponse: %q", resp.State) - } - return nil -} - -func findServicePlan(name string, plans []v1alpha1.ServicePlan) *v1alpha1.ServicePlan { - for _, plan := range plans { - if name == plan.Name { - return &plan - } - } - - return nil -} - -// updateInstanceCondition updates the given condition for the given Instance -// with the given status, reason, and message. -func (c *controller) updateInstanceCondition( - instance *v1alpha1.Instance, - conditionType v1alpha1.InstanceConditionType, - status v1alpha1.ConditionStatus, - reason, message string) error { - - clone, err := api.Scheme.DeepCopy(instance) - if err != nil { - return err - } - toUpdate := clone.(*v1alpha1.Instance) - - newCondition := v1alpha1.InstanceCondition{ - Type: conditionType, - Status: status, - Reason: reason, - Message: message, - } - - t := time.Now() - - if len(instance.Status.Conditions) == 0 { - glog.Infof(`Setting lastTransitionTime for Instance "%v/%v" condition %q to %v`, instance.Namespace, instance.Name, conditionType, t) - newCondition.LastTransitionTime = metav1.NewTime(t) - toUpdate.Status.Conditions = []v1alpha1.InstanceCondition{newCondition} - } else { - for i, cond := range instance.Status.Conditions { - if cond.Type == conditionType { - if cond.Status != newCondition.Status { - glog.Infof(`Found status change for Instance "%v/%v" condition %q: %q -> %q; setting lastTransitionTime to %v`, instance.Namespace, instance.Name, conditionType, cond.Status, status, t) - newCondition.LastTransitionTime = metav1.NewTime(t) - } else { - newCondition.LastTransitionTime = cond.LastTransitionTime - } - - toUpdate.Status.Conditions[i] = newCondition - break - } - } - } - - glog.V(4).Infof("Updating %v condition for Instance %v/%v to %v", conditionType, instance.Namespace, instance.Name, status) - _, err = c.serviceCatalogClient.Instances(instance.Namespace).UpdateStatus(toUpdate) - if err != nil { - glog.Errorf("Failed to update condition %v for Instance %v/%v to true: %v", conditionType, instance.Namespace, instance.Name, err) - } - - return err -} - -// updateInstanceFinalizers updates the given finalizers for the given Binding. -func (c *controller) updateInstanceFinalizers( - instance *v1alpha1.Instance, - finalizers []string) error { - - // Get the latest version of the instance so that we can avoid conflicts - // (since we have probably just updated the status of the instance and are - // now removing the last finalizer). - instance, err := c.serviceCatalogClient.Instances(instance.Namespace).Get(instance.Name, metav1.GetOptions{}) - if err != nil { - glog.Errorf("Error getting Instance %v/%v to finalize: %v", instance.Namespace, instance.Name, err) - } - - clone, err := api.Scheme.DeepCopy(instance) - if err != nil { - return err - } - toUpdate := clone.(*v1alpha1.Instance) - - toUpdate.Finalizers = finalizers - - logContext := fmt.Sprintf("finalizers for Instance %v/%v to %v", - instance.Namespace, instance.Name, finalizers) - - glog.V(4).Infof("Updating %v", logContext) - _, err = c.serviceCatalogClient.Instances(instance.Namespace).UpdateStatus(toUpdate) - if err != nil { - glog.Errorf("Error updating %v: %v", logContext, err) - } - return err -} - -func (c *controller) instanceDelete(obj interface{}) { - instance, ok := obj.(*v1alpha1.Instance) - if instance == nil || !ok { - return - } - - glog.V(4).Infof("Received delete event for Instance %v/%v", instance.Namespace, instance.Name) -} - -// Binding handlers and control-loop - -func (c *controller) bindingAdd(obj interface{}) { - key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) - if err != nil { - glog.Errorf("Couldn't get key for object %+v: %v", obj, err) - return - } - c.bindingQueue.Add(key) -} - -func (c *controller) reconcileBindingKey(key string) error { - namespace, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - return err - } - binding, err := c.bindingLister.Bindings(namespace).Get(name) - if errors.IsNotFound(err) { - glog.Infof("Not doing work for Binding %v because it has been deleted", key) - return nil - } - if err != nil { - glog.Infof("Unable to retrieve Binding %v from store: %v", key, err) - return err - } - - return c.reconcileBinding(binding) -} - -func (c *controller) bindingUpdate(oldObj, newObj interface{}) { - c.bindingAdd(newObj) -} - -func (c *controller) reconcileBinding(binding *v1alpha1.Binding) error { - // Determine whether the checksum has been invalidated by a change to the - // object. If the binding's checksum matches the calculated checksum, - // there is no work to do. - // - // We only do this if the deletion timestamp is nil, because the deletion - // timestamp changes the object's state in a way that we must reconcile, - // but does not affect the checksum. - if binding.Status.Checksum != nil && binding.DeletionTimestamp == nil { - bindingChecksum := checksum.BindingSpecChecksum(binding.Spec) - if bindingChecksum == *binding.Status.Checksum { - glog.V(4).Infof("Not processing event for Binding %v/%v because checksum showed there is no work to do", binding.Namespace, binding.Name) - return nil - } - } - - glog.V(4).Infof("Processing Binding %v/%v", binding.Namespace, binding.Name) - - instance, err := c.instanceLister.Instances(binding.Namespace).Get(binding.Spec.InstanceRef.Name) - if err != nil { - s := fmt.Sprintf("Binding \"%s/%s\" references a non-existent Instance \"%s/%s\"", binding.Namespace, binding.Name, binding.Namespace, binding.Spec.InstanceRef.Name) - glog.Warning(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorNonexistentInstanceReason, - "The binding references an Instance that does not exist. "+s, - ) - c.recorder.Event(binding, api.EventTypeWarning, errorNonexistentInstanceReason, s) - return err - } - - if instance.Status.AsyncOpInProgress { - s := fmt.Sprintf("Binding \"%s/%s\" trying to bind to Instance \"%s/%s\" that has ongoing asynchronous operation", binding.Namespace, binding.Name, binding.Namespace, binding.Spec.InstanceRef.Name) - glog.Info(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorWithOngoingAsyncOperation, - errorWithOngoingAsyncOperationMessage, - ) - c.recorder.Event(binding, api.EventTypeWarning, errorWithOngoingAsyncOperation, s) - return fmt.Errorf("Ongoing Asynchronous operation") - } - - serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBrokerForBinding(instance, binding) - if err != nil { - return err - } - - if !isPlanBindable(serviceClass, servicePlan) { - s := fmt.Sprintf("Binding \"%s/%s\" references a non-bindable ServiceClass (%q) and Plan (%q) combination", binding.Namespace, binding.Name, instance.Spec.ServiceClassName, instance.Spec.PlanName) - glog.Warning(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorNonbindableServiceClassReason, - s, - ) - c.recorder.Event(binding, api.EventTypeWarning, errorNonbindableServiceClassReason, s) - return err - } - - if binding.DeletionTimestamp == nil { // Add or update - glog.V(4).Infof("Adding/Updating Binding %v/%v", binding.Namespace, binding.Name) - - var parameters map[string]interface{} - if binding.Spec.Parameters != nil { - parameters, err = unmarshalParameters(binding.Spec.Parameters.Raw) - if err != nil { - s := fmt.Sprintf("Failed to unmarshal Binding parameters\n%s\n %s", binding.Spec.Parameters, err) - glog.Warning(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorWithParameters, - "Error unmarshaling binding parameters. "+s, - ) - c.recorder.Event(binding, api.EventTypeWarning, errorWithParameters, s) - return err - } - } - - ns, err := c.kubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) - if err != nil { - s := fmt.Sprintf("Failed to get namespace %q during binding: %s", instance.Namespace, err) - glog.Info(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorFindingNamespaceInstanceReason, - "Error finding namespace for instance. "+s, - ) - c.recorder.Eventf(binding, api.EventTypeWarning, errorFindingNamespaceInstanceReason, s) - return err - } - - if !isInstanceReady(instance) { - s := fmt.Sprintf(`Binding cannot begin because referenced instance "%v/%v" is not ready`, instance.Namespace, instance.Name) - glog.Info(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorInstanceNotReadyReason, - s, - ) - c.recorder.Eventf(binding, api.EventTypeWarning, errorInstanceNotReadyReason, s) - return err - } - - request := &brokerapi.BindingRequest{ - ServiceID: serviceClass.ExternalID, - PlanID: servicePlan.ExternalID, - Parameters: parameters, - AppGUID: string(ns.UID), - BindResource: map[string]interface{}{"app_guid": string(ns.UID)}, - } - response, err := brokerClient.CreateServiceBinding(instance.Spec.ExternalID, binding.Spec.ExternalID, request) - if err != nil { - s := fmt.Sprintf("Error creating Binding \"%s/%s\" for Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", binding.Name, binding.Namespace, instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) - glog.Warning(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorBindCallReason, - "Bind call failed. "+s) - c.recorder.Event(binding, api.EventTypeWarning, errorBindCallReason, s) - return err - } - err = c.injectBinding(binding, &response.Credentials) - if err != nil { - s := fmt.Sprintf("Error injecting binding results for Binding \"%s/%s\": %s", binding.Namespace, binding.Name, err) - glog.Warning(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorInjectingBindResultReason, - "Error injecting bind result "+s, - ) - c.recorder.Event(binding, api.EventTypeWarning, errorInjectingBindResultReason, s) - return err - } - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionTrue, - successInjectedBindResultReason, - successInjectedBindResultMessage, - ) - c.recorder.Event(binding, api.EventTypeNormal, successInjectedBindResultReason, successInjectedBindResultMessage) - - glog.V(5).Infof("Successfully bound to Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) - - return nil - } - - // All updates not having a DeletingTimestamp will have been handled above - // and returned early. If we reach this point, we're dealing with an update - // that's actually a soft delete-- i.e. we have some finalization to do. - // Since the potential exists for a binding to have multiple finalizers and - // since those most be cleared in order, we proceed with the soft delete - // only if it's "our turn--" i.e. only if the finalizer we care about is at - // the head of the finalizers list. - // TODO: Should we use a more specific string here? - if len(binding.Finalizers) > 0 && binding.Finalizers[0] == "kubernetes" { - glog.V(4).Infof("Finalizing Binding %v/%v", binding.Namespace, binding.Name) - err = c.ejectBinding(binding) - if err != nil { - s := fmt.Sprintf("Error deleting secret: %s", err) - glog.Warning(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionUnknown, - errorEjectingBindReason, - errorEjectingBindMessage+s, - ) - c.recorder.Eventf(binding, api.EventTypeWarning, errorEjectingBindReason, "%v %v", errorEjectingBindMessage, s) - return err - } - err = brokerClient.DeleteServiceBinding(instance.Spec.ExternalID, binding.Spec.ExternalID, serviceClass.ExternalID, servicePlan.ExternalID) - if err != nil { - s := fmt.Sprintf("Error unbinding Binding \"%s/%s\" for Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", binding.Name, binding.Namespace, instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) - glog.Warning(s) - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - errorUnbindCallReason, - "Unbind call failed. "+s) - c.recorder.Event(binding, api.EventTypeWarning, errorUnbindCallReason, s) - return err - } - - c.updateBindingCondition( - binding, - v1alpha1.BindingConditionReady, - v1alpha1.ConditionFalse, - successUnboundReason, - "The binding was deleted successfully", - ) - // Clear the finalizer - c.updateBindingFinalizers(binding, binding.Finalizers[1:]) - c.recorder.Event(binding, api.EventTypeNormal, successUnboundReason, "This binding was deleted successfully") - - glog.V(5).Infof("Successfully deleted Binding %v/%v of Instance %v/%v of ServiceClass %v at Broker %v", binding.Namespace, binding.Name, instance.Namespace, instance.Name, serviceClass.Name, brokerName) - } - - return nil -} - -// isPlanBindable returns whether the given ServiceClass and ServicePlan -// combination is bindable. Plans may override the service-level bindable -// attribute, so if the plan provides a value, return that value. Otherwise, -// return the Bindable field of the ServiceClass. -// -// Note: enforcing that the plan belongs to the given service class is the -// responsibility of the caller. -func isPlanBindable(serviceClass *v1alpha1.ServiceClass, plan *v1alpha1.ServicePlan) bool { - if plan.Bindable != nil { - return *plan.Bindable - } - - return serviceClass.Bindable -} - -func (c *controller) injectBinding(binding *v1alpha1.Binding, credentials *brokerapi.Credential) error { - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: binding.Spec.SecretName, - Namespace: binding.Namespace, - }, - Data: make(map[string][]byte), - } - - for k, v := range *credentials { - var err error - secret.Data[k], err = serialize(v) - if err != nil { - return fmt.Errorf("Unable to serialize credential value %q: %v; %s", - k, v, err) - } - } - - found := false - - _, err := c.kubeClient.Core().Secrets(binding.Namespace).Get(binding.Spec.SecretName, metav1.GetOptions{}) - if err == nil { - found = true - } - - if found { - _, err = c.kubeClient.Core().Secrets(binding.Namespace).Update(secret) - } else { - _, err = c.kubeClient.Core().Secrets(binding.Namespace).Create(secret) - } - - return err -} - -func (c *controller) ejectBinding(binding *v1alpha1.Binding) error { - _, err := c.kubeClient.Core().Secrets(binding.Namespace).Get(binding.Spec.SecretName, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return nil - } - - glog.Errorf("Error getting secret %v/%v: %v", binding.Namespace, binding.Spec.SecretName, err) - return err - } - - glog.V(5).Infof("Deleting secret %v/%v", binding.Namespace, binding.Spec.SecretName) - err = c.kubeClient.Core().Secrets(binding.Namespace).Delete(binding.Spec.SecretName, &metav1.DeleteOptions{}) - - return err -} - -// updateBindingCondition updates the given condition for the given Binding -// with the given status, reason, and message. -func (c *controller) updateBindingCondition( - binding *v1alpha1.Binding, - conditionType v1alpha1.BindingConditionType, - status v1alpha1.ConditionStatus, - reason, message string) error { - - clone, err := api.Scheme.DeepCopy(binding) - if err != nil { - return err - } - toUpdate := clone.(*v1alpha1.Binding) - - newCondition := v1alpha1.BindingCondition{ - Type: conditionType, - Status: status, - Reason: reason, - Message: message, - } - - t := time.Now() - - if len(binding.Status.Conditions) == 0 { - glog.Infof(`Setting lastTransitionTime for Binding "%v/%v" condition %q to %v`, binding.Namespace, binding.Name, conditionType, t) - newCondition.LastTransitionTime = metav1.NewTime(t) - toUpdate.Status.Conditions = []v1alpha1.BindingCondition{newCondition} - } else { - for i, cond := range binding.Status.Conditions { - if cond.Type == conditionType { - if cond.Status != newCondition.Status { - glog.Infof(`Found status change for Binding "%v/%v" condition %q: %q -> %q; setting lastTransitionTime to %v`, binding.Namespace, binding.Name, conditionType, cond.Status, status, t) - newCondition.LastTransitionTime = metav1.NewTime(time.Now()) - } else { - newCondition.LastTransitionTime = cond.LastTransitionTime - } - - toUpdate.Status.Conditions[i] = newCondition - break - } - } - } - - logContext := fmt.Sprintf("%v condition for Binding %v/%v to %v (Reason: %q, Message: %q)", - conditionType, binding.Namespace, binding.Name, status, reason, message) - glog.V(4).Infof("Updating %v", logContext) - _, err = c.serviceCatalogClient.Bindings(binding.Namespace).UpdateStatus(toUpdate) - if err != nil { - glog.Errorf("Error updating %v: %v", logContext, err) - } - return err -} - -// updateBindingFinalizers updates the given finalizers for the given Binding. -func (c *controller) updateBindingFinalizers( - binding *v1alpha1.Binding, - finalizers []string) error { - - // Get the latest version of the binding so that we can avoid conflicts - // (since we have probably just updated the status of the binding and are - // now removing the last finalizer). - binding, err := c.serviceCatalogClient.Bindings(binding.Namespace).Get(binding.Name, metav1.GetOptions{}) - if err != nil { - glog.Errorf("Error getting Binding %v/%v to finalize: %v", binding.Namespace, binding.Name, err) - } - - clone, err := api.Scheme.DeepCopy(binding) - if err != nil { - return err - } - toUpdate := clone.(*v1alpha1.Binding) - - toUpdate.Finalizers = finalizers - - logContext := fmt.Sprintf("finalizers for Binding %v/%v to %v", - binding.Namespace, binding.Name, finalizers) - - glog.V(4).Infof("Updating %v", logContext) - _, err = c.serviceCatalogClient.Bindings(binding.Namespace).UpdateStatus(toUpdate) - if err != nil { - glog.Errorf("Error updating %v: %v", logContext, err) - } - return err -} - -func (c *controller) bindingDelete(obj interface{}) { - binding, ok := obj.(*v1alpha1.Binding) - if binding == nil || !ok { - return - } - - glog.V(4).Infof("Received delete event for Binding %v/%v", binding.Namespace, binding.Name) -} - // getServiceClassPlanAndBroker is a sequence of operations that's done in couple of // places so this method fetches the Service Class, Service Plan and creates // a brokerClient to use for that method given an Instance. @@ -1670,11 +350,19 @@ func (c *controller) getServiceClassPlanAndBrokerForBinding(instance *v1alpha1.I // returns an error. If the AuthSecret field is nil, empty values are // returned. func getAuthCredentialsFromBroker(client kubernetes.Interface, broker *v1alpha1.Broker) (username, password string, err error) { - if broker.Spec.AuthSecret == nil { + // TODO: when we start supporting additional auth schemes, this code will have to accommodate + // the new schemes + if broker.Spec.AuthInfo == nil { return "", "", nil } - authSecret, err := client.Core().Secrets(broker.Spec.AuthSecret.Namespace).Get(broker.Spec.AuthSecret.Name, metav1.GetOptions{}) + basicAuthSecret := broker.Spec.AuthInfo.BasicAuthSecret + + if basicAuthSecret == nil { + return "", "", nil + } + + authSecret, err := client.Core().Secrets(basicAuthSecret.Namespace).Get(basicAuthSecret.Name, metav1.GetOptions{}) if err != nil { return "", "", err } @@ -1734,6 +422,7 @@ func convertServicePlans(plans []brokerapi.ServicePlan) ([]v1alpha1.ServicePlan, Free: plans[i].Free, Description: plans[i].Description, } + if plans[i].Bindable != nil { b := *plans[i].Bindable ret[i].Bindable = &b @@ -1749,6 +438,40 @@ func convertServicePlans(plans []brokerapi.ServicePlan) ([]v1alpha1.ServicePlan, ret[i].ExternalMetadata = &runtime.RawExtension{Raw: metadata} } + if schemas := plans[i].Schemas; schemas != nil { + if instanceSchemas := schemas.ServiceInstances; instanceSchemas != nil { + if instanceCreateSchema := instanceSchemas.Create; instanceCreateSchema != nil && instanceCreateSchema.Parameters != nil { + schema, err := json.Marshal(instanceCreateSchema.Parameters) + if err != nil { + err = fmt.Errorf("Failed to marshal instance create schema \n%+v\n %v", instanceCreateSchema.Parameters, err) + glog.Error(err) + return nil, err + } + ret[i].AlphaInstanceCreateParameterSchema = &runtime.RawExtension{Raw: schema} + } + if instanceUpdateSchema := instanceSchemas.Update; instanceUpdateSchema != nil && instanceUpdateSchema.Parameters != nil { + schema, err := json.Marshal(instanceUpdateSchema.Parameters) + if err != nil { + err = fmt.Errorf("Failed to marshal instance update schema \n%+v\n %v", instanceUpdateSchema.Parameters, err) + glog.Error(err) + return nil, err + } + ret[i].AlphaInstanceUpdateParameterSchema = &runtime.RawExtension{Raw: schema} + } + } + if bindingSchemas := schemas.ServiceBindings; bindingSchemas != nil { + if bindingCreateSchema := bindingSchemas.Create; bindingCreateSchema != nil && bindingCreateSchema.Parameters != nil { + schema, err := json.Marshal(bindingCreateSchema.Parameters) + if err != nil { + err = fmt.Errorf("Failed to marshal binding create schema \n%+v\n %v", bindingCreateSchema.Parameters, err) + glog.Error(err) + return nil, err + } + ret[i].AlphaBindingCreateParameterSchema = &runtime.RawExtension{Raw: schema} + } + } + } + } return ret, nil } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_binding.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_binding.go new file mode 100644 index 000000000000..31a9f3150163 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_binding.go @@ -0,0 +1,508 @@ +/* +Copyright 2017 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 controller + +import ( + "fmt" + "time" + + "github.com/golang/glog" + checksum "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/checksum/versioned/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + settingsv1alpha1 "k8s.io/client-go/pkg/apis/settings/v1alpha1" + "k8s.io/client-go/tools/cache" +) + +// Binding handlers and control-loop + +func (c *controller) bindingAdd(obj interface{}) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + glog.Errorf("Couldn't get key for object %+v: %v", obj, err) + return + } + c.bindingQueue.Add(key) +} + +func (c *controller) reconcileBindingKey(key string) error { + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + binding, err := c.bindingLister.Bindings(namespace).Get(name) + if errors.IsNotFound(err) { + glog.Infof("Not doing work for Binding %v because it has been deleted", key) + return nil + } + if err != nil { + glog.Infof("Unable to retrieve Binding %v from store: %v", key, err) + return err + } + + return c.reconcileBinding(binding) +} + +func (c *controller) bindingUpdate(oldObj, newObj interface{}) { + c.bindingAdd(newObj) +} + +func (c *controller) reconcileBinding(binding *v1alpha1.Binding) error { + // Determine whether the checksum has been invalidated by a change to the + // object. If the binding's checksum matches the calculated checksum, + // there is no work to do. + // + // We only do this if the deletion timestamp is nil, because the deletion + // timestamp changes the object's state in a way that we must reconcile, + // but does not affect the checksum. + if binding.Status.Checksum != nil && binding.DeletionTimestamp == nil { + bindingChecksum := checksum.BindingSpecChecksum(binding.Spec) + if bindingChecksum == *binding.Status.Checksum { + glog.V(4).Infof( + "Not processing event for Binding %v/%v because checksum showed there is no work to do", + binding.Namespace, + binding.Name, + ) + return nil + } + } + + glog.V(4).Infof("Processing Binding %v/%v", binding.Namespace, binding.Name) + + instance, err := c.instanceLister.Instances(binding.Namespace).Get(binding.Spec.InstanceRef.Name) + if err != nil { + s := fmt.Sprintf("Binding \"%s/%s\" references a non-existent Instance \"%s/%s\"", binding.Namespace, binding.Name, binding.Namespace, binding.Spec.InstanceRef.Name) + glog.Warningf( + "Binding %s/%s references a non-existent instance %s/%s (%s)", + binding.Namespace, + binding.Name, + binding.Namespace, + binding.Spec.InstanceRef.Name, + err, + ) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorNonexistentInstanceReason, + "The binding references an Instance that does not exist. "+s, + ) + c.recorder.Event(binding, api.EventTypeWarning, errorNonexistentInstanceReason, s) + return err + } + + if instance.Status.AsyncOpInProgress { + s := fmt.Sprintf( + "Binding \"%s/%s\" trying to bind to Instance \"%s/%s\" that has ongoing asynchronous operation", + binding.Namespace, + binding.Name, + binding.Namespace, + binding.Spec.InstanceRef.Name, + ) + glog.Info(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorWithOngoingAsyncOperation, + errorWithOngoingAsyncOperationMessage, + ) + c.recorder.Event(binding, api.EventTypeWarning, errorWithOngoingAsyncOperation, s) + return fmt.Errorf("Ongoing Asynchronous operation") + } + + serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBrokerForBinding(instance, binding) + if err != nil { + return err + } + + if !isPlanBindable(serviceClass, servicePlan) { + s := fmt.Sprintf( + "Binding \"%s/%s\" references a non-bindable ServiceClass (%q) and Plan (%q) combination", + binding.Namespace, + binding.Name, + instance.Spec.ServiceClassName, + instance.Spec.PlanName, + ) + glog.Warning(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorNonbindableServiceClassReason, + s, + ) + c.recorder.Event(binding, api.EventTypeWarning, errorNonbindableServiceClassReason, s) + return err + } + + if binding.DeletionTimestamp == nil { // Add or update + glog.V(4).Infof("Adding/Updating Binding %v/%v", binding.Namespace, binding.Name) + + var parameters map[string]interface{} + if binding.Spec.Parameters != nil { + parameters, err = unmarshalParameters(binding.Spec.Parameters.Raw) + if err != nil { + s := fmt.Sprintf("Failed to unmarshal Binding parameters\n%s\n %s", binding.Spec.Parameters, err) + glog.Warning(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorWithParameters, + "Error unmarshaling binding parameters. "+s, + ) + c.recorder.Event(binding, api.EventTypeWarning, errorWithParameters, s) + return err + } + } + + ns, err := c.kubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) + if err != nil { + s := fmt.Sprintf("Failed to get namespace %q during binding: %s", instance.Namespace, err) + glog.Info(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorFindingNamespaceInstanceReason, + "Error finding namespace for instance. "+s, + ) + c.recorder.Eventf(binding, api.EventTypeWarning, errorFindingNamespaceInstanceReason, s) + return err + } + + if !isInstanceReady(instance) { + s := fmt.Sprintf(`Binding cannot begin because referenced instance "%v/%v" is not ready`, instance.Namespace, instance.Name) + glog.Info(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorInstanceNotReadyReason, + s, + ) + c.recorder.Eventf(binding, api.EventTypeWarning, errorInstanceNotReadyReason, s) + return err + } + + request := &brokerapi.BindingRequest{ + ServiceID: serviceClass.ExternalID, + PlanID: servicePlan.ExternalID, + Parameters: parameters, + AppGUID: string(ns.UID), + BindResource: map[string]interface{}{"app_guid": string(ns.UID)}, + } + response, err := brokerClient.CreateServiceBinding(instance.Spec.ExternalID, binding.Spec.ExternalID, request) + if err != nil { + s := fmt.Sprintf("Error creating Binding \"%s/%s\" for Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", binding.Name, binding.Namespace, instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) + glog.Warning(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorBindCallReason, + "Bind call failed. "+s) + c.recorder.Event(binding, api.EventTypeWarning, errorBindCallReason, s) + return err + } + err = c.injectBinding(binding, &response.Credentials) + if err != nil { + s := fmt.Sprintf("Error injecting binding results for Binding \"%s/%s\": %s", binding.Namespace, binding.Name, err) + glog.Warning(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorInjectingBindResultReason, + "Error injecting bind result "+s, + ) + c.recorder.Event(binding, api.EventTypeWarning, errorInjectingBindResultReason, s) + return err + } + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionTrue, + successInjectedBindResultReason, + successInjectedBindResultMessage, + ) + c.recorder.Event(binding, api.EventTypeNormal, successInjectedBindResultReason, successInjectedBindResultMessage) + + glog.V(5).Infof("Successfully bound to Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + + return nil + } + + // All updates not having a DeletingTimestamp will have been handled above + // and returned early. If we reach this point, we're dealing with an update + // that's actually a soft delete-- i.e. we have some finalization to do. + // Since the potential exists for a binding to have multiple finalizers and + // since those most be cleared in order, we proceed with the soft delete + // only if it's "our turn--" i.e. only if the finalizer we care about is at + // the head of the finalizers list. + if finalizers := sets.NewString(binding.Finalizers...); finalizers.Has(v1alpha1.FinalizerServiceCatalog) { + glog.V(4).Infof("Finalizing Binding %v/%v", binding.Namespace, binding.Name) + err = c.ejectBinding(binding) + if err != nil { + s := fmt.Sprintf("Error deleting secret: %s", err) + glog.Warning(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionUnknown, + errorEjectingBindReason, + errorEjectingBindMessage+s, + ) + c.recorder.Eventf(binding, api.EventTypeWarning, errorEjectingBindReason, "%v %v", errorEjectingBindMessage, s) + return err + } + err = brokerClient.DeleteServiceBinding(instance.Spec.ExternalID, binding.Spec.ExternalID, serviceClass.ExternalID, servicePlan.ExternalID) + if err != nil { + s := fmt.Sprintf( + "Error unbinding Binding \"%s/%s\" for Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", + binding.Name, + binding.Namespace, + instance.Namespace, + instance.Name, + serviceClass.Name, + brokerName, + err, + ) + glog.Warning(s) + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + errorUnbindCallReason, + "Unbind call failed. "+s) + c.recorder.Event(binding, api.EventTypeWarning, errorUnbindCallReason, s) + return err + } + + c.updateBindingCondition( + binding, + v1alpha1.BindingConditionReady, + v1alpha1.ConditionFalse, + successUnboundReason, + "The binding was deleted successfully", + ) + // Clear the finalizer + finalizers.Delete(v1alpha1.FinalizerServiceCatalog) + c.updateBindingFinalizers(binding, finalizers.List()) + c.recorder.Event(binding, api.EventTypeNormal, successUnboundReason, "This binding was deleted successfully") + + glog.V(5).Infof("Successfully deleted Binding %v/%v of Instance %v/%v of ServiceClass %v at Broker %v", binding.Namespace, binding.Name, instance.Namespace, instance.Name, serviceClass.Name, brokerName) + } + + return nil +} + +// isPlanBindable returns whether the given ServiceClass and ServicePlan +// combination is bindable. Plans may override the service-level bindable +// attribute, so if the plan provides a value, return that value. Otherwise, +// return the Bindable field of the ServiceClass. +// +// Note: enforcing that the plan belongs to the given service class is the +// responsibility of the caller. +func isPlanBindable(serviceClass *v1alpha1.ServiceClass, plan *v1alpha1.ServicePlan) bool { + if plan.Bindable != nil { + return *plan.Bindable + } + + return serviceClass.Bindable +} + +func (c *controller) injectBinding(binding *v1alpha1.Binding, credentials *brokerapi.Credential) error { + glog.V(5).Infof("Creating Secret %v/%v", binding.Namespace, binding.Spec.SecretName) + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: binding.Spec.SecretName, + Namespace: binding.Namespace, + }, + Data: make(map[string][]byte), + } + + for k, v := range *credentials { + var err error + secret.Data[k], err = serialize(v) + if err != nil { + return fmt.Errorf("Unable to serialize credential value %q: %v; %s", + k, v, err) + } + } + + found := false + + _, err := c.kubeClient.Core().Secrets(binding.Namespace).Get(binding.Spec.SecretName, metav1.GetOptions{}) + if err == nil { + found = true + } + + if found { + _, err = c.kubeClient.Core().Secrets(binding.Namespace).Update(secret) + } else { + _, err = c.kubeClient.Core().Secrets(binding.Namespace).Create(secret) + } + + if err != nil || binding.Spec.AlphaPodPresetTemplate == nil { + return err + } + + podPreset := &settingsv1alpha1.PodPreset{ + ObjectMeta: metav1.ObjectMeta{ + Name: binding.Spec.AlphaPodPresetTemplate.Name, + Namespace: binding.Namespace, + }, + Spec: settingsv1alpha1.PodPresetSpec{ + Selector: binding.Spec.AlphaPodPresetTemplate.Selector, + EnvFrom: []v1.EnvFromSource{ + { + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: binding.Spec.SecretName, + }, + }, + }, + }, + }, + } + + _, err = c.kubeClient.SettingsV1alpha1().PodPresets(binding.Namespace).Create(podPreset) + + return err +} + +func (c *controller) ejectBinding(binding *v1alpha1.Binding) error { + var err error + + if binding.Spec.AlphaPodPresetTemplate != nil { + podPresetName := binding.Spec.AlphaPodPresetTemplate.Name + glog.V(5).Infof("Deleting PodPreset %v/%v", binding.Namespace, podPresetName) + err := c.kubeClient.SettingsV1alpha1().PodPresets(binding.Namespace).Delete(podPresetName, &metav1.DeleteOptions{}) + if err != nil && !errors.IsNotFound(err) { + return err + } + } + + glog.V(5).Infof("Deleting Secret %v/%v", binding.Namespace, binding.Spec.SecretName) + err = c.kubeClient.Core().Secrets(binding.Namespace).Delete(binding.Spec.SecretName, &metav1.DeleteOptions{}) + if err != nil && !errors.IsNotFound(err) { + return err + } + + return nil +} + +// updateBindingCondition updates the given condition for the given Binding +// with the given status, reason, and message. +func (c *controller) updateBindingCondition( + binding *v1alpha1.Binding, + conditionType v1alpha1.BindingConditionType, + status v1alpha1.ConditionStatus, + reason, message string) error { + + clone, err := api.Scheme.DeepCopy(binding) + if err != nil { + return err + } + toUpdate := clone.(*v1alpha1.Binding) + + newCondition := v1alpha1.BindingCondition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + } + + t := time.Now() + + if len(binding.Status.Conditions) == 0 { + glog.Infof(`Setting lastTransitionTime for Binding "%v/%v" condition %q to %v`, binding.Namespace, binding.Name, conditionType, t) + newCondition.LastTransitionTime = metav1.NewTime(t) + toUpdate.Status.Conditions = []v1alpha1.BindingCondition{newCondition} + } else { + for i, cond := range binding.Status.Conditions { + if cond.Type == conditionType { + if cond.Status != newCondition.Status { + glog.Infof(`Found status change for Binding "%v/%v" condition %q: %q -> %q; setting lastTransitionTime to %v`, binding.Namespace, binding.Name, conditionType, cond.Status, status, t) + newCondition.LastTransitionTime = metav1.NewTime(time.Now()) + } else { + newCondition.LastTransitionTime = cond.LastTransitionTime + } + + toUpdate.Status.Conditions[i] = newCondition + break + } + } + } + + logContext := fmt.Sprintf("%v condition for Binding %v/%v to %v (Reason: %q, Message: %q)", + conditionType, binding.Namespace, binding.Name, status, reason, message) + glog.V(4).Infof("Updating %v", logContext) + _, err = c.serviceCatalogClient.Bindings(binding.Namespace).UpdateStatus(toUpdate) + if err != nil { + glog.Errorf("Error updating %v: %v", logContext, err) + } + return err +} + +// updateBindingFinalizers updates the given finalizers for the given Binding. +func (c *controller) updateBindingFinalizers( + binding *v1alpha1.Binding, + finalizers []string) error { + + // Get the latest version of the binding so that we can avoid conflicts + // (since we have probably just updated the status of the binding and are + // now removing the last finalizer). + binding, err := c.serviceCatalogClient.Bindings(binding.Namespace).Get(binding.Name, metav1.GetOptions{}) + if err != nil { + glog.Errorf("Error getting Binding %v/%v to finalize: %v", binding.Namespace, binding.Name, err) + } + + clone, err := api.Scheme.DeepCopy(binding) + if err != nil { + return err + } + toUpdate := clone.(*v1alpha1.Binding) + + toUpdate.Finalizers = finalizers + + logContext := fmt.Sprintf("finalizers for Binding %v/%v to %v", + binding.Namespace, binding.Name, finalizers) + + glog.V(4).Infof("Updating %v", logContext) + _, err = c.serviceCatalogClient.Bindings(binding.Namespace).UpdateStatus(toUpdate) + if err != nil { + glog.Errorf("Error updating %v: %v", logContext, err) + } + return err +} + +func (c *controller) bindingDelete(obj interface{}) { + binding, ok := obj.(*v1alpha1.Binding) + if binding == nil || !ok { + return + } + + glog.V(4).Infof("Received delete event for Binding %v/%v", binding.Namespace, binding.Name) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_binding_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_binding_test.go new file mode 100644 index 000000000000..51f2519192b7 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_binding_test.go @@ -0,0 +1,769 @@ +/* +Copyright 2017 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 controller + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "testing" + "time" + + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + fakebrokerapi "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + settingsv1alpha1 "k8s.io/client-go/pkg/apis/settings/v1alpha1" + clientgotesting "k8s.io/client-go/testing" +) + +func TestReconcileBindingNonExistingInstance(t *testing.T) { + _, fakeCatalogClient, _, testController, _ := newTestController(t) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: "nothere"}, + ExternalID: bindingGUID, + }, + } + + testController.reconcileBinding(binding) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says it failed because no such instance exists. + updateAction := actions[0].(clientgotesting.UpdateAction) + if e, a := "update", updateAction.GetVerb(); e != a { + t.Fatalf("Unexpected verb on actions[0]; expected %v, got %v", e, a) + } + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding, errorNonexistentInstanceReason) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorNonexistentInstanceReason + " " + "Binding \"/test-binding\" references a non-existent Instance \"/nothere\"" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBindingNonExistingServiceClass(t *testing.T) { + _, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + instance := &v1alpha1.Instance{ + ObjectMeta: metav1.ObjectMeta{Name: testInstanceName, Namespace: testNamespace}, + Spec: v1alpha1.InstanceSpec{ + ServiceClassName: "nothere", + PlanName: testPlanName, + ExternalID: instanceGUID, + }, + } + sharedInformers.Instances().Informer().GetStore().Add(instance) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + testController.reconcileBinding(binding) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says it failed because no such service class. + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding, errorNonexistentServiceClassMessage) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorNonexistentServiceClassMessage + " " + "Binding \"test-ns/test-binding\" references a non-existent ServiceClass \"nothere\"" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBindingWithParameters(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + testNsUID := "test_ns_uid" + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(testNsUID), + }, + }, nil + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestInstanceWithStatus(v1alpha1.ConditionTrue)) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + parameters := bindingParameters{Name: "test-param"} + parameters.Args = append(parameters.Args, "first-arg") + parameters.Args = append(parameters.Args, "second-arg") + b, err := json.Marshal(parameters) + if err != nil { + t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) + } + binding.Spec.Parameters = &runtime.RawExtension{Raw: b} + + testController.reconcileBinding(binding) + + if testNsUID != fakeBrokerClient.Bindings[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)].AppID { + t.Fatalf("Unexpected broker AppID: expected %q, got %q", testNsUID, fakeBrokerClient.Bindings[instanceGUID+":"+bindingGUID].AppID) + } + + bindResource := fakeBrokerClient.BindingRequests[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)].BindResource + if appGUID := bindResource["app_guid"]; testNsUID != fmt.Sprintf("%v", appGUID) { + t.Fatalf("Unexpected broker AppID: expected %q, got %q", testNsUID, appGUID) + } + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says binding was created + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyTrue(t, updatedBinding) + + updateObject, ok := updatedBinding.(*v1alpha1.Binding) + if !ok { + t.Fatalf("couldn't convert to *v1alpha1.Binding") + } + + // Verify parameters are what we'd expect them to be, basically name, array with two values in it. + if len(updateObject.Spec.Parameters.Raw) == 0 { + t.Fatalf("Parameters was unexpectedly empty") + } + if b, ok := fakeBrokerClient.BindingClient.Bindings[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)]; !ok { + t.Fatalf("Did not find the created Binding in fakeInstanceBinding after creation") + } else { + if len(b.Parameters) == 0 { + t.Fatalf("Expected parameters, but got none") + } + if e, a := "test-param", b.Parameters["name"].(string); e != a { + t.Fatalf("Unexpected name for parameters: expected %v, got %v", e, a) + } + argsArray := b.Parameters["args"].([]interface{}) + if len(argsArray) != 2 { + t.Fatalf("Expected 2 elements in args array, but got %d", len(argsArray)) + } + foundFirst := false + foundSecond := false + for _, el := range argsArray { + if el.(string) == "first-arg" { + foundFirst = true + } + if el.(string) == "second-arg" { + foundSecond = true + } + } + if !foundFirst { + t.Fatalf("Failed to find 'first-arg' in array, was %v", argsArray) + } + if !foundSecond { + t.Fatalf("Failed to find 'second-arg' in array, was %v", argsArray) + } + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successInjectedBindResultReason + " " + successInjectedBindResultMessage + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBindingNonbindableServiceClass(t *testing.T) { + _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestNonbindableServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestNonbindableInstance()) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + testController.reconcileBinding(binding) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says binding was created + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding, errorNonbindableServiceClassReason) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorNonbindableServiceClassReason + ` Binding "test-ns/test-binding" references a non-bindable ServiceClass ("test-unbindable-serviceclass") and Plan ("test-unbindable-plan") combination` + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBindingNonbindableServiceClassBindablePlan(t *testing.T) { + _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestNonbindableServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(func() *v1alpha1.Instance { + i := getTestInstanceNonbindableServiceBindablePlan() + i.Status = v1alpha1.InstanceStatus{ + Conditions: []v1alpha1.InstanceCondition{ + { + Type: v1alpha1.InstanceConditionReady, + Status: v1alpha1.ConditionTrue, + }, + }, + } + return i + }()) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + testController.reconcileBinding(binding) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says binding was created + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyTrue(t, updatedBinding) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) +} + +func TestReconcileBindingBindableServiceClassNonbindablePlan(t *testing.T) { + _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestInstanceBindableServiceNonbindablePlan()) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + testController.reconcileBinding(binding) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says binding was created + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding, errorNonbindableServiceClassReason) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorNonbindableServiceClassReason + ` Binding "test-ns/test-binding" references a non-bindable ServiceClass ("test-serviceclass") and Plan ("test-unbindable-plan") combination` + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBindingFailsWithInstanceAsyncOngoing(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestInstanceAsyncProvisioning("")) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + err := testController.reconcileBinding(binding) + if err == nil { + t.Fatalf("reconcileBinding did not fail with async operation ongoing") + } + + if !strings.Contains(err.Error(), "Ongoing Asynchronous") { + t.Fatalf("Did not get the expected error %q : got %q", "Ongoing Asynchronous", err) + } + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says binding was created + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + if !strings.Contains(events[0], "has ongoing asynchronous operation") { + t.Fatalf("Did not find expected error %q : got %q", "has ongoing asynchronous operation", events[0]) + } + if !strings.Contains(events[0], testNamespace+"/"+testInstanceName) { + t.Fatalf("Did not find expected instance name : got %q", events[0]) + } + if !strings.Contains(events[0], testNamespace+"/"+testBindingName) { + t.Fatalf("Did not find expected binding name : got %q", events[0]) + } +} + +func TestReconcileBindingInstanceNotReady(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID("test_ns_uid"), + }, + }, nil + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestInstance()) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + testController.reconcileBinding(binding) + + if _, ok := fakeBrokerClient.Bindings[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)]; ok { + t.Fatalf("Unexpected broker binding call") + } + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says binding was created + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorInstanceNotReadyReason + " " + `Binding cannot begin because referenced instance "test-ns/test-instance" is not ready` + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBindingNamespaceError(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{}, errors.New("No namespace") + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestInstance()) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + }, + } + + testController.reconcileBinding(binding) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorFindingNamespaceInstanceReason + " " + "Failed to get namespace \"test-ns\" during binding: No namespace" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBindingDelete(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + bindingsMapKey := fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID) + + fakeBrokerClient.BindingClient.Bindings = map[string]*brokerapi.ServiceBinding{bindingsMapKey: {}} + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestInstance()) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{ + Name: testBindingName, + Namespace: testNamespace, + DeletionTimestamp: &metav1.Time{}, + Finalizers: []string{v1alpha1.FinalizerServiceCatalog}, + }, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + SecretName: testBindingSecretName, + }, + } + + fakeCatalogClient.AddReactor("get", "bindings", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, binding, nil + }) + + testController.reconcileBinding(binding) + + kubeActions := fakeKubeClient.Actions() + // The two actions should be: + // 0. Deleting the secret + assertNumberOfActions(t, kubeActions, 1) + + deleteAction := kubeActions[0].(clientgotesting.DeleteActionImpl) + if e, a := "delete", deleteAction.GetVerb(); e != a { + t.Fatalf("Unexpected verb on kubeActions[1]; expected %v, got %v", e, a) + } + + if e, a := binding.Spec.SecretName, deleteAction.Name; e != a { + t.Fatalf("Unexpected name of secret: expected %v, got %v", e, a) + } + + actions := fakeCatalogClient.Actions() + // The three actions should be: + // 0. Updating the ready condition + // 1. Get against the binding in question + // 2. Removing the finalizer + assertNumberOfActions(t, actions, 3) + + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyFalse(t, updatedBinding) + + assertGet(t, actions[1], binding) + + updatedBinding = assertUpdateStatus(t, actions[2], binding) + assertEmptyFinalizers(t, updatedBinding) + + if _, ok := fakeBrokerClient.BindingClient.Bindings[bindingsMapKey]; ok { + t.Fatalf("Found the deleted Binding in fakeBindingClient after deletion") + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successUnboundReason + " " + "This binding was deleted successfully" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +const testPodPresetName = "test-pod-preset" + +func TestReconcileBindingWithPodPresetTemplate(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + testNsUID := "test_ns_uid" + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(testNsUID), + }, + }, nil + }) + + fakeKubeClient.AddReactor("get", "secrets", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("not found") + }) + + fakeKubeClient.AddReactor("create", "podpresets", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + sharedInformers.Instances().Informer().GetStore().Add(getTestInstanceWithStatus(v1alpha1.ConditionTrue)) + + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, + SecretName: testBindingSecretName, + AlphaPodPresetTemplate: &v1alpha1.AlphaPodPresetTemplate{ + Name: testPodPresetName, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + } + + testController.reconcileBinding(binding) + + if testNsUID != fakeBrokerClient.Bindings[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)].AppID { + t.Fatalf("Unexpected broker AppID: expected %q, got %q", testNsUID, fakeBrokerClient.Bindings[instanceGUID+":"+bindingGUID].AppID) + } + + bindResource := fakeBrokerClient.BindingRequests[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)].BindResource + if appGUID := bindResource["app_guid"]; testNsUID != fmt.Sprintf("%v", appGUID) { + t.Fatalf("Unexpected broker AppID: expected %q, got %q", testNsUID, appGUID) + } + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says binding was created + updatedBinding := assertUpdateStatus(t, actions[0], binding) + assertBindingReadyTrue(t, updatedBinding) + + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 4) + + action := kubeActions[2].(clientgotesting.CreateAction) + if e, a := "create", action.GetVerb(); e != a { + t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) + } + if e, a := "secrets", action.GetResource().Resource; e != a { + t.Fatalf("Unexpected resource on action; expected %v, got %v", e, a) + } + actionSecret, ok := action.GetObject().(*v1.Secret) + if !ok { + t.Fatal("couldn't convert secret into a v1.Secret") + } + if e, a := testBindingSecretName, actionSecret.Name; e != a { + t.Fatalf("Unexpected name of secret; expected %v, got %v", e, a) + } + + action = kubeActions[3].(clientgotesting.CreateAction) + if e, a := "create", action.GetVerb(); e != a { + t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) + } + if e, a := "podpresets", action.GetResource().Resource; e != a { + t.Fatalf("Unexpected resource on action; expected %v, got %v", e, a) + } + actionPodPreset, ok := action.GetObject().(*settingsv1alpha1.PodPreset) + if !ok { + t.Fatal("couldn't convert PodPreset into a settingsv1alpha1.PodPreset") + } + if e, a := testPodPresetName, actionPodPreset.Name; e != a { + t.Fatalf("Unexpected name of PodPreset; expected %v, got %v", e, a) + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successInjectedBindResultReason + " " + successInjectedBindResultMessage + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestUpdateBindingCondition(t *testing.T) { + getTestBindingWithStatus := func(status v1alpha1.ConditionStatus) *v1alpha1.Binding { + instance := getTestBinding() + instance.Status = v1alpha1.BindingStatus{ + Conditions: []v1alpha1.BindingCondition{{ + Type: v1alpha1.BindingConditionReady, + Status: status, + Message: "message", + LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), + }}, + } + + return instance + } + + cases := []struct { + name string + input *v1alpha1.Binding + status v1alpha1.ConditionStatus + reason string + message string + transitionTimeChanged bool + }{ + + { + name: "initially unset", + input: getTestBinding(), + status: v1alpha1.ConditionFalse, + transitionTimeChanged: true, + }, + { + name: "not ready -> not ready", + input: getTestBindingWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionFalse, + transitionTimeChanged: false, + }, + { + name: "not ready -> not ready, message and reason change", + input: getTestBindingWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionFalse, + reason: "foo", + message: "bar", + transitionTimeChanged: false, + }, + { + name: "not ready -> ready", + input: getTestBindingWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionTrue, + transitionTimeChanged: true, + }, + { + name: "ready -> ready", + input: getTestBindingWithStatus(v1alpha1.ConditionTrue), + status: v1alpha1.ConditionTrue, + transitionTimeChanged: false, + }, + { + name: "ready -> not ready", + input: getTestBindingWithStatus(v1alpha1.ConditionTrue), + status: v1alpha1.ConditionFalse, + transitionTimeChanged: true, + }, + } + + for _, tc := range cases { + _, fakeCatalogClient, _, testController, _ := newTestController(t) + + clone, err := api.Scheme.DeepCopy(tc.input) + if err != nil { + t.Errorf("%v: deep copy failed", tc.name) + continue + } + inputClone := clone.(*v1alpha1.Binding) + + err = testController.updateBindingCondition(tc.input, v1alpha1.BindingConditionReady, tc.status, tc.reason, tc.message) + if err != nil { + t.Errorf("%v: error updating broker condition: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(tc.input, inputClone) { + t.Errorf("%v: updating broker condition mutated input: expected %v, got %v", tc.name, inputClone, tc.input) + continue + } + + actions := fakeCatalogClient.Actions() + if ok := expectNumberOfActions(t, tc.name, actions, 1); !ok { + continue + } + + updatedBinding, ok := expectUpdateStatus(t, tc.name, actions[0], tc.input) + if !ok { + continue + } + + updateActionObject, ok := updatedBinding.(*v1alpha1.Binding) + if !ok { + t.Errorf("%v: couldn't convert to binding", tc.name) + continue + } + + var initialTs metav1.Time + if len(inputClone.Status.Conditions) != 0 { + initialTs = inputClone.Status.Conditions[0].LastTransitionTime + } + + if e, a := 1, len(updateActionObject.Status.Conditions); e != a { + t.Errorf("%v: expected %v condition(s), got %v", tc.name, e, a) + } + + outputCondition := updateActionObject.Status.Conditions[0] + newTs := outputCondition.LastTransitionTime + + if tc.transitionTimeChanged && initialTs == newTs { + t.Errorf("%v: transition time didn't change when it should have", tc.name) + continue + } else if !tc.transitionTimeChanged && initialTs != newTs { + t.Errorf("%v: transition time changed when it shouldn't have", tc.name) + continue + } + if e, a := tc.reason, outputCondition.Reason; e != "" && e != a { + t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) + continue + } + if e, a := tc.message, outputCondition.Message; e != "" && e != a { + t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) + } + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_broker.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_broker.go new file mode 100644 index 000000000000..8e925391992f --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_broker.go @@ -0,0 +1,436 @@ +/* +Copyright 2017 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 controller + +import ( + stderrors "errors" + "fmt" + "time" + + "github.com/golang/glog" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/tools/cache" +) + +func (c *controller) brokerAdd(obj interface{}) { + // DeletionHandlingMetaNamespaceKeyFunc returns a unique key for the resource and + // handles the special case where the resource is of DeletedFinalStateUnknown type, which + // acts a place holder for resources that have been deleted from storage but the watch event + // confirming the deletion has not yet arrived. + // Generally, the key is "namespace/name" for namespaced-scoped resources and + // just "name" for cluster scoped resources. + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + glog.Errorf("Couldn't get key for object %+v: %v", obj, err) + return + } + c.brokerQueue.Add(key) +} + +func (c *controller) brokerUpdate(oldObj, newObj interface{}) { + c.brokerAdd(newObj) +} + +func (c *controller) brokerDelete(obj interface{}) { + broker, ok := obj.(*v1alpha1.Broker) + if broker == nil || !ok { + return + } + + glog.V(4).Infof("Received delete event for Broker %v", broker.Name) +} + +// the Message strings have a terminating period and space so they can +// be easily combined with a follow on specific message. +const ( + errorFetchingCatalogReason string = "ErrorFetchingCatalog" + errorFetchingCatalogMessage string = "Error fetching catalog. " + errorSyncingCatalogReason string = "ErrorSyncingCatalog" + errorSyncingCatalogMessage string = "Error syncing catalog from Broker. " + errorWithParameters string = "ErrorWithParameters" + errorListingServiceClassesReason string = "ErrorListingServiceClasses" + errorListingServiceClassesMessage string = "Error listing service classes." + errorDeletingServiceClassReason string = "ErrorDeletingServiceClass" + errorDeletingServiceClassMessage string = "Error deleting service class." + errorNonexistentServiceClassReason string = "ReferencesNonexistentServiceClass" + errorNonexistentServiceClassMessage string = "ReferencesNonexistentServiceClass" + errorNonexistentServicePlanReason string = "ReferencesNonexistentServicePlan" + errorNonexistentBrokerReason string = "ReferencesNonexistentBroker" + errorNonexistentInstanceReason string = "ReferencesNonexistentInstance" + errorAuthCredentialsReason string = "ErrorGettingAuthCredentials" + errorFindingNamespaceInstanceReason string = "ErrorFindingNamespaceForInstance" + errorProvisionCalledReason string = "ProvisionCallFailed" + errorDeprovisionCalledReason string = "DeprovisionCallFailed" + errorBindCallReason string = "BindCallFailed" + errorInjectingBindResultReason string = "ErrorInjectingBindResult" + errorEjectingBindReason string = "ErrorEjectingBinding" + errorEjectingBindMessage string = "Error ejecting binding." + errorUnbindCallReason string = "UnbindCallFailed" + errorWithOngoingAsyncOperation string = "ErrorAsyncOperationInProgress" + errorWithOngoingAsyncOperationMessage string = "Another operation for this service instance is in progress. " + errorNonbindableServiceClassReason string = "ErrorNonbindableServiceClass" + errorInstanceNotReadyReason string = "ErrorInstanceNotReady" + + successInjectedBindResultReason string = "InjectedBindResult" + successInjectedBindResultMessage string = "Injected bind result" + successDeprovisionReason string = "DeprovisionedSuccessfully" + successDeprovisionMessage string = "The instance was deprovisioned successfully" + successProvisionReason string = "ProvisionedSuccessfully" + successProvisionMessage string = "The instance was provisioned successfully" + successFetchedCatalogReason string = "FetchedCatalog" + successFetchedCatalogMessage string = "Successfully fetched catalog entries from broker." + successBrokerDeletedReason string = "DeletedSuccessfully" + successBrokerDeletedMessage string = "The broker %v was deleted successfully." + successUnboundReason string = "UnboundSuccessfully" + asyncProvisioningReason string = "Provisioning" + asyncProvisioningMessage string = "The instance is being provisioned asynchronously" + asyncDeprovisioningReason string = "Derovisioning" + asyncDeprovisioningMessage string = "The instance is being deprovisioned asynchronously" +) + +// shouldReconcileBroker determines whether a broker should be reconciled; it +// returns true unless the broker has a ready condition with status true and +// the controller's broker relist interval has not elapsed since the broker's +// ready condition became true. +func shouldReconcileBroker(broker *v1alpha1.Broker, now time.Time, relistInterval time.Duration) bool { + if broker.DeletionTimestamp != nil || len(broker.Status.Conditions) == 0 { + // If the deletion timestamp is set or the broker has no status + // conditions, we should reconcile it. + return true + } + + // find the ready condition in the broker's status + for _, condition := range broker.Status.Conditions { + if condition.Type == v1alpha1.BrokerConditionReady { + // The broker has a ready condition + + if condition.Status == v1alpha1.ConditionTrue { + // The broker's ready condition has status true, meaning that + // at some point, we successfully listed the broker's catalog. + // We should reconcile the broker (relist the broker's + // catalog) if it has been longer than the configured relist + // interval since the broker's ready condition became true. + return now.After(condition.LastTransitionTime.Add(relistInterval)) + } + + // The broker's ready condition wasn't true; we should try to re- + // list the broker. + return true + } + } + + // The broker didn't have a ready condition; we should reconcile it. + return true +} + +func (c *controller) reconcileBrokerKey(key string) error { + broker, err := c.brokerLister.Get(key) + if errors.IsNotFound(err) { + glog.Infof("Not doing work for Broker %v because it has been deleted", key) + return nil + } + if err != nil { + glog.Infof("Unable to retrieve Broker %v from store: %v", key, err) + return err + } + + return c.reconcileBroker(broker) +} + +// reconcileBroker is the control-loop that reconciles a Broker. +func (c *controller) reconcileBroker(broker *v1alpha1.Broker) error { + glog.V(4).Infof("Processing Broker %v", broker.Name) + + // If the broker's ready condition is true and the relist interval has not + // elapsed, do not reconcile it. + if !shouldReconcileBroker(broker, time.Now(), c.brokerRelistInterval) { + glog.V(10).Infof( + "Not processing Broker %v because relist interval has not elapsed since the broker became ready", + broker.Name, + ) + return nil + } + + username, password, err := getAuthCredentialsFromBroker(c.kubeClient, broker) + if err != nil { + s := fmt.Sprintf("Error getting broker auth credentials for broker %q: %s", broker.Name, err) + glog.Info(s) + c.recorder.Event(broker, api.EventTypeWarning, errorAuthCredentialsReason, s) + c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorFetchingCatalogReason, errorFetchingCatalogMessage+s) + return err + } + + glog.V(4).Infof("Creating client for Broker %v, URL: %v", broker.Name, broker.Spec.URL) + brokerClient := c.brokerClientCreateFunc(broker.Name, broker.Spec.URL, username, password) + + if broker.DeletionTimestamp == nil { // Add or update + glog.V(4).Infof("Adding/Updating Broker %v", broker.Name) + brokerCatalog, err := brokerClient.GetCatalog() + if err != nil { + s := fmt.Sprintf("Error getting broker catalog for broker %q: %s", broker.Name, err) + glog.Warning(s) + c.recorder.Eventf(broker, api.EventTypeWarning, errorFetchingCatalogReason, s) + c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorFetchingCatalogReason, + errorFetchingCatalogMessage+s) + return err + } + glog.V(5).Infof("Successfully fetched %v catalog entries for Broker %v", len(brokerCatalog.Services), broker.Name) + + glog.V(4).Infof("Converting catalog response for Broker %v into service-catalog API", broker.Name) + catalog, err := convertCatalog(brokerCatalog) + if err != nil { + s := fmt.Sprintf("Error converting catalog payload for broker %q to service-catalog API: %s", broker.Name, err) + glog.Warning(s) + c.recorder.Eventf(broker, api.EventTypeWarning, errorSyncingCatalogReason, s) + c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorSyncingCatalogReason, errorSyncingCatalogMessage+s) + return err + } + glog.V(5).Infof("Successfully converted catalog payload from Broker %v to service-catalog API", broker.Name) + + if len(catalog) == 0 { + s := fmt.Sprintf("Error getting catalog payload for broker %q; received zero services; at least one service is required", broker.Name) + glog.Warning(s) + c.recorder.Eventf(broker, api.EventTypeWarning, errorSyncingCatalogReason, s) + c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorSyncingCatalogReason, errorSyncingCatalogMessage+s) + return stderrors.New(s) + } + + for _, serviceClass := range catalog { + glog.V(4).Infof("Reconciling serviceClass %v (broker %v)", serviceClass.Name, broker.Name) + if err := c.reconcileServiceClassFromBrokerCatalog(broker, serviceClass); err != nil { + s := fmt.Sprintf( + "Error reconciling serviceClass %q (broker %q): %s", + serviceClass.Name, + broker.Name, + err, + ) + glog.Warning(s) + c.recorder.Eventf(broker, api.EventTypeWarning, errorSyncingCatalogReason, s) + c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionFalse, errorSyncingCatalogReason, + errorSyncingCatalogMessage+s) + return err + } + + glog.V(5).Infof("Reconciled serviceClass %v (broker %v)", serviceClass.Name, broker.Name) + } + + c.updateBrokerCondition(broker, v1alpha1.BrokerConditionReady, v1alpha1.ConditionTrue, successFetchedCatalogReason, successFetchedCatalogMessage) + c.recorder.Event(broker, api.EventTypeNormal, successFetchedCatalogReason, successFetchedCatalogMessage) + return nil + } + + // All updates not having a DeletingTimestamp will have been handled above + // and returned early. If we reach this point, we're dealing with an update + // that's actually a soft delete-- i.e. we have some finalization to do. + // Since the potential exists for a broker to have multiple finalizers and + // since those most be cleared in order, we proceed with the soft delete + // only if it's "our turn--" i.e. only if the finalizer we care about is at + // the head of the finalizers list. + if finalizers := sets.NewString(broker.Finalizers...); finalizers.Has(v1alpha1.FinalizerServiceCatalog) { + glog.V(4).Infof("Finalizing Broker %v", broker.Name) + + // Get ALL ServiceClasses. Remove those that reference this Broker. + svcClasses, err := c.serviceClassLister.List(labels.Everything()) + if err != nil { + c.updateBrokerCondition( + broker, + v1alpha1.BrokerConditionReady, + v1alpha1.ConditionUnknown, + errorListingServiceClassesReason, + errorListingServiceClassesMessage, + ) + c.recorder.Eventf(broker, api.EventTypeWarning, errorListingServiceClassesReason, "%v %v", errorListingServiceClassesMessage, err) + return err + } + + // Delete ServiceClasses that are for THIS Broker. + for _, svcClass := range svcClasses { + if svcClass.BrokerName == broker.Name { + err := c.serviceCatalogClient.ServiceClasses().Delete(svcClass.Name, &metav1.DeleteOptions{}) + if err != nil && !errors.IsNotFound(err) { + s := fmt.Sprintf( + "Error deleting ServiceClass %q (Broker %q): %s", + svcClass.Name, + broker.Name, + err, + ) + glog.Warning(s) + c.updateBrokerCondition( + broker, + v1alpha1.BrokerConditionReady, + v1alpha1.ConditionUnknown, + errorDeletingServiceClassMessage, + errorDeletingServiceClassReason+s, + ) + c.recorder.Eventf(broker, api.EventTypeWarning, errorDeletingServiceClassReason, "%v %v", errorDeletingServiceClassMessage, s) + return err + } + } + } + + c.updateBrokerCondition( + broker, + v1alpha1.BrokerConditionReady, + v1alpha1.ConditionFalse, + successBrokerDeletedReason, + "The broker was deleted successfully", + ) + // Clear the finalizer + finalizers.Delete(v1alpha1.FinalizerServiceCatalog) + c.updateBrokerFinalizers(broker, finalizers.List()) + + c.recorder.Eventf(broker, api.EventTypeNormal, successBrokerDeletedReason, successBrokerDeletedMessage, broker.Name) + glog.V(5).Infof("Successfully deleted Broker %v", broker.Name) + return nil + } + + return nil +} + +// reconcileServiceClassFromBrokerCatalog reconciles a ServiceClass after the +// Broker's catalog has been re-listed. +func (c *controller) reconcileServiceClassFromBrokerCatalog(broker *v1alpha1.Broker, serviceClass *v1alpha1.ServiceClass) error { + serviceClass.BrokerName = broker.Name + + existingServiceClass, err := c.serviceClassLister.Get(serviceClass.Name) + if errors.IsNotFound(err) { + // An error returned from a lister Get call means that the object does + // not exist. Create a new ServiceClass. + if _, err := c.serviceCatalogClient.ServiceClasses().Create(serviceClass); err != nil { + glog.Errorf("Error creating serviceClass %v from Broker %v: %v", serviceClass.Name, broker.Name, err) + return err + } + + return nil + } else if err != nil { + glog.Errorf("Error getting serviceClass %v: %v", serviceClass.Name, err) + return err + } + + if existingServiceClass.BrokerName != broker.Name { + errMsg := fmt.Sprintf("ServiceClass %q for Broker %q already exists for Broker %q", serviceClass.Name, broker.Name, existingServiceClass.BrokerName) + glog.Error(errMsg) + return fmt.Errorf(errMsg) + } + + if existingServiceClass.ExternalID != serviceClass.ExternalID { + errMsg := fmt.Sprintf("ServiceClass %q already exists with OSB guid %q, received different guid %q", serviceClass.Name, existingServiceClass.ExternalID, serviceClass.ExternalID) + glog.Error(errMsg) + return fmt.Errorf(errMsg) + } + + glog.V(5).Infof("Found existing serviceClass %v; updating", serviceClass.Name) + + // There was an existing service class -- project the update onto it and + // update it. + clone, err := api.Scheme.DeepCopy(existingServiceClass) + if err != nil { + return err + } + + toUpdate := clone.(*v1alpha1.ServiceClass) + toUpdate.Bindable = serviceClass.Bindable + toUpdate.Plans = serviceClass.Plans + toUpdate.PlanUpdatable = serviceClass.PlanUpdatable + toUpdate.AlphaTags = serviceClass.AlphaTags + toUpdate.Description = serviceClass.Description + toUpdate.AlphaRequires = serviceClass.AlphaRequires + + if _, err := c.serviceCatalogClient.ServiceClasses().Update(toUpdate); err != nil { + glog.Errorf("Error updating serviceClass %v from Broker %v: %v", serviceClass.Name, broker.Name, err) + return err + } + + return nil +} + +// updateBrokerReadyCondition updates the ready condition for the given Broker +// with the given status, reason, and message. +func (c *controller) updateBrokerCondition(broker *v1alpha1.Broker, conditionType v1alpha1.BrokerConditionType, status v1alpha1.ConditionStatus, reason, message string) error { + clone, err := api.Scheme.DeepCopy(broker) + if err != nil { + return err + } + toUpdate := clone.(*v1alpha1.Broker) + newCondition := v1alpha1.BrokerCondition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + } + + t := time.Now() + + if len(broker.Status.Conditions) == 0 { + glog.Infof("Setting lastTransitionTime for Broker %q condition %q to %v", broker.Name, conditionType, t) + newCondition.LastTransitionTime = metav1.NewTime(t) + toUpdate.Status.Conditions = []v1alpha1.BrokerCondition{newCondition} + } else { + for i, cond := range broker.Status.Conditions { + if cond.Type == conditionType { + if cond.Status != newCondition.Status { + glog.Infof("Found status change for Broker %q condition %q: %q -> %q; setting lastTransitionTime to %v", broker.Name, conditionType, cond.Status, status, t) + newCondition.LastTransitionTime = metav1.NewTime(t) + } else { + newCondition.LastTransitionTime = cond.LastTransitionTime + } + + toUpdate.Status.Conditions[i] = newCondition + break + } + } + } + + glog.V(4).Infof("Updating ready condition for Broker %v to %v", broker.Name, status) + _, err = c.serviceCatalogClient.Brokers().UpdateStatus(toUpdate) + if err != nil { + glog.Errorf("Error updating ready condition for Broker %v: %v", broker.Name, err) + } else { + glog.V(5).Infof("Updated ready condition for Broker %v to %v", broker.Name, status) + } + + return err +} + +// updateBrokerFinalizers updates the given finalizers for the given Broker. +func (c *controller) updateBrokerFinalizers( + broker *v1alpha1.Broker, + finalizers []string) error { + + clone, err := api.Scheme.DeepCopy(broker) + if err != nil { + return err + } + toUpdate := clone.(*v1alpha1.Broker) + + toUpdate.Finalizers = finalizers + + logContext := fmt.Sprintf("finalizers for Broker %v to %v", + broker.Name, finalizers) + + glog.V(4).Infof("Updating %v", logContext) + _, err = c.serviceCatalogClient.Brokers().UpdateStatus(toUpdate) + if err != nil { + glog.Errorf("Error updating %v: %v", logContext, err) + } + return err +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_broker_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_broker_test.go new file mode 100644 index 000000000000..f6e06a805800 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_broker_test.go @@ -0,0 +1,534 @@ +/* +Copyright 2017 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 controller + +import ( + "errors" + "reflect" + "testing" + "time" + + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + fakebrokerapi "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake" + fakebrokerserver "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + clientgotesting "k8s.io/client-go/testing" +) + +func TestShouldReconcileBroker(t *testing.T) { + cases := []struct { + name string + broker *v1alpha1.Broker + now time.Time + interval time.Duration + reconcile bool + }{ + { + name: "no status", + broker: getTestBroker(), + now: time.Now(), + interval: 3 * time.Minute, + reconcile: true, + }, + { + name: "no ready condition", + broker: func() *v1alpha1.Broker { + b := getTestBroker() + b.Status = v1alpha1.BrokerStatus{ + Conditions: []v1alpha1.BrokerCondition{ + { + Type: v1alpha1.BrokerConditionType("NotARealCondition"), + Status: v1alpha1.ConditionTrue, + }, + }, + } + return b + }(), + now: time.Now(), + interval: 3 * time.Minute, + reconcile: true, + }, + { + name: "not ready", + broker: getTestBrokerWithStatus(v1alpha1.ConditionFalse), + now: time.Now(), + interval: 3 * time.Minute, + reconcile: true, + }, + { + name: "ready, interval elapsed", + broker: func() *v1alpha1.Broker { + broker := getTestBrokerWithStatus(v1alpha1.ConditionTrue) + return broker + }(), + now: time.Now(), + interval: 3 * time.Minute, + reconcile: true, + }, + { + name: "ready, interval not elapsed", + broker: func() *v1alpha1.Broker { + broker := getTestBrokerWithStatus(v1alpha1.ConditionTrue) + return broker + }(), + now: time.Now(), + interval: 3 * time.Hour, + reconcile: false, + }, + } + + for _, tc := range cases { + var ltt *time.Time + if len(tc.broker.Status.Conditions) != 0 { + ltt = &tc.broker.Status.Conditions[0].LastTransitionTime.Time + } + + t.Logf("%v: now: %v, interval: %v, last transition time: %v", tc.name, tc.now, tc.interval, ltt) + actual := shouldReconcileBroker(tc.broker, tc.now, tc.interval) + + if e, a := tc.reconcile, actual; e != a { + t.Errorf("%v: unexpected result: expected %v, got %v", tc.name, e, a) + } + } +} + +func TestReconcileBroker(t *testing.T) { + const ( + brokerUsername = "testuser" + brokerPassword = "testpassword" + ) + controllerParams, err := newTestControllerWithBrokerServer(brokerUsername, brokerPassword) + if err != nil { + t.Fatal(err) + } + defer controllerParams.Close() + + controllerParams.BrokerServerHandler.Catalog = fakebrokerserver.ConvertCatalog(getTestCatalog()) + controllerParams.Controller.reconcileBroker(getTestBroker()) + + actions := controllerParams.FakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 2) + + // first action should be a create action for a service class + assertCreate(t, actions[0], getTestServiceClass()) + + // second action should be an update action for broker status subresource + updatedBroker := assertUpdateStatus(t, actions[1], getTestBroker()) + assertBrokerReadyTrue(t, updatedBroker) + + // verify no kube resources created + assertNumberOfActions(t, controllerParams.FakeKubeClient.Actions(), 0) + + events := getRecordedEvents(controllerParams.Controller) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successFetchedCatalogReason + " " + successFetchedCatalogMessage + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } + + if controllerParams.BrokerServerHandler.CatalogRequests != 1 { + t.Fatalf( + "expected 1 catalog request, got %d", + controllerParams.BrokerServerHandler.CatalogRequests, + ) + } +} + +func TestReconcileBrokerExistingServiceClass(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + testServiceClass := getTestServiceClass() + sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + testController.reconcileBroker(getTestBroker()) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 2) + + // first action should be an update action for a service class + assertUpdate(t, actions[0], testServiceClass) + + // second action should be an update action for broker status subresource + updatedBroker := assertUpdateStatus(t, actions[1], getTestBroker()) + assertBrokerReadyTrue(t, updatedBroker) + + // verify no kube resources created + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) +} + +func TestReconcileBrokerExistingServiceClassDifferentExternalID(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + testServiceClass := getTestServiceClass() + testServiceClass.ExternalID = "notTheSame" + sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + testController.reconcileBroker(getTestBroker()) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedBroker := assertUpdateStatus(t, actions[0], getTestBroker()) + assertBrokerReadyFalse(t, updatedBroker) + + // verify no kube resources created + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorSyncingCatalogReason + ` Error reconciling serviceClass "test-serviceclass" (broker "test-broker"): ServiceClass "test-serviceclass" already exists with OSB guid "notTheSame", received different guid "SCGUID"` + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event; expected\n%v, got\n%v", e, a) + } +} + +func TestReconcileBrokerExistingServiceClassDifferentBroker(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + testServiceClass := getTestServiceClass() + testServiceClass.BrokerName = "notTheSame" + sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + testController.reconcileBroker(getTestBroker()) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedBroker := assertUpdateStatus(t, actions[0], getTestBroker()) + assertBrokerReadyFalse(t, updatedBroker) + + // verify no kube resources created + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorSyncingCatalogReason + ` Error reconciling serviceClass "test-serviceclass" (broker "test-broker"): ServiceClass "test-serviceclass" for Broker "test-broker" already exists for Broker "notTheSame"` + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event; expected\n%v, got\n%v", e, a) + } +} + +func TestReconcileBrokerDelete(t *testing.T) { + fakeKubeClient, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) + + testServiceClass := getTestServiceClass() + sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) + + broker := getTestBroker() + broker.DeletionTimestamp = &metav1.Time{} + broker.Finalizers = []string{v1alpha1.FinalizerServiceCatalog} + + testController.reconcileBroker(broker) + + // Verify no core kube actions occurred + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + // The three actions should be: + // 0. Deleting the associated ServiceClass + // 1. Updating the ready condition + // 2. Removing the finalizer + assertNumberOfActions(t, actions, 3) + + assertDelete(t, actions[0], testServiceClass) + + updatedBroker := assertUpdateStatus(t, actions[1], broker) + assertBrokerReadyFalse(t, updatedBroker) + + updatedBroker = assertUpdateStatus(t, actions[2], broker) + assertEmptyFinalizers(t, updatedBroker) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successBrokerDeletedReason + " " + "The broker test-broker was deleted successfully." + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBrokerErrorFetchingCatalog(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, _ := newTestController(t) + + fakeBrokerClient.CatalogClient.RetErr = fakebrokerapi.ErrInstanceNotFound + broker := getTestBroker() + + testController.reconcileBroker(broker) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedBroker := assertUpdateStatus(t, actions[0], broker) + assertBrokerReadyFalse(t, updatedBroker) + + assertNumberOfActions(t, fakeKubeClient.Actions(), 0) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorFetchingCatalogReason + " " + "Error getting broker catalog for broker \"test-broker\": instance not found" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBrokerZeroServices(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, _ := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = &brokerapi.Catalog{ + Services: []*brokerapi.Service{}, + } + broker := getTestBroker() + + testController.reconcileBroker(broker) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedBroker := assertUpdateStatus(t, actions[0], broker) + assertBrokerReadyFalse(t, updatedBroker) + + assertNumberOfActions(t, fakeKubeClient.Actions(), 0) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorSyncingCatalogReason + ` Error getting catalog payload for broker "test-broker"; received zero services; at least one service is required` + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event; \nexpected: %v\ngot: %v", e, a) + } +} + +func TestReconcileBrokerWithAuthError(t *testing.T) { + fakeKubeClient, fakeCatalogClient, _, testController, _ := newTestController(t) + + broker := getTestBroker() + broker.Spec.AuthInfo = &v1alpha1.BrokerAuthInfo{ + BasicAuthSecret: &v1.ObjectReference{ + Namespace: "does_not_exist", + Name: "auth-name", + }, + } + + fakeKubeClient.AddReactor("get", "secrets", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("no secret defined") + }) + + testController.reconcileBroker(broker) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedBroker := assertUpdateStatus(t, actions[0], broker) + assertBrokerReadyFalse(t, updatedBroker) + + // verify one kube action occurred + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 1) + + getAction := kubeActions[0].(clientgotesting.GetAction) + if e, a := "get", getAction.GetVerb(); e != a { + t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) + } + if e, a := "secrets", getAction.GetResource().Resource; e != a { + t.Fatalf("Unexpected resource on action; expected %v, got %v", e, a) + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorAuthCredentialsReason + " " + "Error getting broker auth credentials for broker \"test-broker\": no secret defined" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileBrokerWithReconcileError(t *testing.T) { + fakeKubeClient, fakeCatalogClient, _, testController, _ := newTestController(t) + + broker := getTestBroker() + broker.Spec.AuthInfo = &v1alpha1.BrokerAuthInfo{ + BasicAuthSecret: &v1.ObjectReference{ + Namespace: "does_not_exist", + Name: "auth-name", + }, + } + + fakeCatalogClient.AddReactor("create", "serviceclasses", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("error creating serviceclass") + }) + + testController.reconcileBroker(broker) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedBroker := assertUpdateStatus(t, actions[0], broker) + assertBrokerReadyFalse(t, updatedBroker) + + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 1) + + getAction := kubeActions[0].(clientgotesting.GetAction) + if e, a := "get", getAction.GetVerb(); e != a { + t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorAuthCredentialsReason + " " + "Error getting broker auth credentials for broker \"test-broker\": auth secret didn't contain username" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestUpdateBrokerCondition(t *testing.T) { + cases := []struct { + name string + input *v1alpha1.Broker + status v1alpha1.ConditionStatus + reason string + message string + transitionTimeChanged bool + }{ + + { + name: "initially unset", + input: getTestBroker(), + status: v1alpha1.ConditionFalse, + transitionTimeChanged: true, + }, + { + name: "not ready -> not ready", + input: getTestBrokerWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionFalse, + transitionTimeChanged: false, + }, + { + name: "not ready -> not ready with reason and message change", + input: getTestBrokerWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionFalse, + reason: "foo", + message: "bar", + transitionTimeChanged: false, + }, + { + name: "not ready -> ready", + input: getTestBrokerWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionTrue, + transitionTimeChanged: true, + }, + { + name: "ready -> ready", + input: getTestBrokerWithStatus(v1alpha1.ConditionTrue), + status: v1alpha1.ConditionTrue, + transitionTimeChanged: false, + }, + { + name: "ready -> not ready", + input: getTestBrokerWithStatus(v1alpha1.ConditionTrue), + status: v1alpha1.ConditionFalse, + transitionTimeChanged: true, + }, + } + + for _, tc := range cases { + _, fakeCatalogClient, _, testController, _ := newTestController(t) + + clone, err := api.Scheme.DeepCopy(tc.input) + if err != nil { + t.Errorf("%v: deep copy failed", tc.name) + continue + } + + inputClone := clone.(*v1alpha1.Broker) + + err = testController.updateBrokerCondition(tc.input, v1alpha1.BrokerConditionReady, tc.status, tc.reason, tc.message) + if err != nil { + t.Errorf("%v: error updating broker condition: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(tc.input, inputClone) { + t.Errorf("%v: updating broker condition mutated input: expected %v, got %v", tc.name, inputClone, tc.input) + continue + } + + actions := fakeCatalogClient.Actions() + if ok := expectNumberOfActions(t, tc.name, actions, 1); !ok { + continue + } + + updatedBroker, ok := expectUpdateStatus(t, tc.name, actions[0], tc.input) + if !ok { + continue + } + + updateActionObject, ok := updatedBroker.(*v1alpha1.Broker) + if !ok { + t.Errorf("%v: couldn't convert to broker", tc.name) + continue + } + + var initialTs metav1.Time + if len(inputClone.Status.Conditions) != 0 { + initialTs = inputClone.Status.Conditions[0].LastTransitionTime + } + + if e, a := 1, len(updateActionObject.Status.Conditions); e != a { + t.Errorf("%v: expected %v condition(s), got %v", tc.name, e, a) + } + + outputCondition := updateActionObject.Status.Conditions[0] + newTs := outputCondition.LastTransitionTime + + if tc.transitionTimeChanged && initialTs == newTs { + t.Errorf("%v: transition time didn't change when it should have", tc.name) + continue + } else if !tc.transitionTimeChanged && initialTs != newTs { + t.Errorf("%v: transition time changed when it shouldn't have", tc.name) + continue + } + if e, a := tc.reason, outputCondition.Reason; e != "" && e != a { + t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) + continue + } + if e, a := tc.message, outputCondition.Message; e != "" && e != a { + t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) + } + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_instance.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_instance.go new file mode 100644 index 000000000000..8fdc4d43ecbb --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_instance.go @@ -0,0 +1,572 @@ +/* +Copyright 2017 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 controller + +import ( + "fmt" + "net/http" + "time" + + "github.com/golang/glog" + checksum "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/checksum/versioned/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/tools/cache" +) + +// Instance handlers and control-loop + +func (c *controller) instanceAdd(obj interface{}) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + glog.Errorf("Couldn't get key for object %+v: %v", obj, err) + return + } + // TODO(vaikas): If the obj (which really is an Instance right?) has + // AsyncOpInProgress flag set, just add it directly to c.pollingQueue + // here? Why shouldn't we?? + c.instanceQueue.Add(key) +} + +func (c *controller) reconcileInstanceKey(key string) error { + // For namespace-scoped resources, SplitMetaNamespaceKey splits the key + // i.e. "namespace/name" into two separate strings + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + instance, err := c.instanceLister.Instances(namespace).Get(name) + if errors.IsNotFound(err) { + glog.Infof("Not doing work for Instance %v because it has been deleted", key) + return nil + } + if err != nil { + glog.Errorf("Unable to retrieve Instance %v from store: %v", key, err) + return err + } + + return c.reconcileInstance(instance) +} + +func (c *controller) instanceUpdate(oldObj, newObj interface{}) { + c.instanceAdd(newObj) +} + +// reconcileInstanceDelete is responsible for handling any instance whose deletion timestamp is set. +func (c *controller) reconcileInstanceDelete(instance *v1alpha1.Instance) error { + // nothing to do... + if instance.DeletionTimestamp == nil { + return nil + } + + finalizerToken := v1alpha1.FinalizerServiceCatalog + finalizers := sets.NewString(instance.Finalizers...) + if !finalizers.Has(finalizerToken) { + return nil + } + + // if there is no op in progress, and the instance was never provisioned, we can just delete. + // this can happen if the service class name referenced never existed. + if !instance.Status.AsyncOpInProgress && instance.Status.Checksum == nil { + finalizers.Delete(finalizerToken) + // Clear the finalizer + return c.updateInstanceFinalizers(instance, finalizers.List()) + } + + // All updates not having a DeletingTimestamp will have been handled above + // and returned early. If we reach this point, we're dealing with an update + // that's actually a soft delete-- i.e. we have some finalization to do. + // Since the potential exists for an instance to have multiple finalizers and + // since those most be cleared in order, we proceed with the soft delete + // only if it's "our turn--" i.e. only if the finalizer we care about is at + // the head of the finalizers list. + serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBroker(instance) + if err != nil { + return err + } + + glog.V(4).Infof("Finalizing Instance %v/%v", instance.Namespace, instance.Name) + + request := &brokerapi.DeleteServiceInstanceRequest{ + ServiceID: serviceClass.ExternalID, + PlanID: servicePlan.ExternalID, + AcceptsIncomplete: true, + } + + glog.V(4).Infof("Deprovisioning Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + response, respCode, err := brokerClient.DeleteServiceInstance(instance.Spec.ExternalID, request) + + if err != nil { + s := fmt.Sprintf( + "Error deprovisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q with status code %d: %s", + instance.Namespace, + instance.Name, + serviceClass.Name, + brokerName, + respCode, + err, + ) + glog.Warning(s) + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionUnknown, + errorDeprovisionCalledReason, + "Deprovision call failed. "+s) + c.recorder.Event(instance, api.EventTypeWarning, errorDeprovisionCalledReason, s) + return err + } + + if respCode == http.StatusAccepted { + glog.V(5).Infof("Received asynchronous de-provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) + if response.Operation != "" { + instance.Status.LastOperation = &response.Operation + } + + // Tag this instance as having an ongoing async operation so we can enforce + // no other operations against it can start. + instance.Status.AsyncOpInProgress = true + + err := c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + asyncDeprovisioningReason, + asyncDeprovisioningMessage, + ) + if err != nil { + return err + } + } else if respCode == http.StatusOK { + err := c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + successDeprovisionReason, + successDeprovisionMessage, + ) + if err != nil { + return err + } + // Clear the finalizer + finalizers.Delete(finalizerToken) + if err = c.updateInstanceFinalizers(instance, finalizers.List()); err != nil { + return err + } + c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) + glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + } else { + // the broker returned a failure response + errorDeprovisionCalledMessage := fmt.Sprintf("deprovision call failed") + err := c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + errorDeprovisionCalledReason, + errorDeprovisionCalledMessage, + ) + if err != nil { + return err + } + c.recorder.Eventf(instance, api.EventTypeWarning, errorDeprovisionCalledReason, errorDeprovisionCalledMessage) + } + return nil +} + +// reconcileInstance is the control-loop for reconciling Instances. +func (c *controller) reconcileInstance(instance *v1alpha1.Instance) error { + + // If there's no async op in progress, determine whether the checksum + // has been invalidated by a change to the object. If the instance's + // checksum matches the calculated checksum, there is no work to do. + // If there's an async op in progress, we need to keep polling, hence + // do not bail if checksum hasn't changed. + // + // We only do this if the deletion timestamp is nil, because the deletion + // timestamp changes the object's state in a way that we must reconcile, + // but does not affect the checksum. + if !instance.Status.AsyncOpInProgress { + if instance.Status.Checksum != nil && instance.DeletionTimestamp == nil { + instanceChecksum := checksum.InstanceSpecChecksum(instance.Spec) + if instanceChecksum == *instance.Status.Checksum { + glog.V(4).Infof( + "Not processing event for Instance %v/%v because checksum showed there is no work to do", + instance.Namespace, + instance.Name, + ) + return nil + } + } + } + + glog.V(4).Infof("Processing Instance %v/%v", instance.Namespace, instance.Name) + + // if the instance is marked for deletion, handle that first. + if instance.ObjectMeta.DeletionTimestamp != nil { + glog.V(4).Infof("Soft-deleting Instance %v/%v", instance.Namespace, instance.Name) + return c.reconcileInstanceDelete(instance) + } + + serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBroker(instance) + if err != nil { + return err + } + + if instance.Status.AsyncOpInProgress { + return c.pollInstance(serviceClass, servicePlan, brokerName, brokerClient, instance) + } + + glog.V(4).Infof("Adding/Updating Instance %v/%v", instance.Namespace, instance.Name) + + var parameters map[string]interface{} + if instance.Spec.Parameters != nil { + parameters, err = unmarshalParameters(instance.Spec.Parameters.Raw) + if err != nil { + s := fmt.Sprintf("Failed to unmarshal Instance parameters\n%s\n %s", instance.Spec.Parameters, err) + glog.Warning(s) + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + errorWithParameters, + "Error unmarshaling instance parameters. "+s, + ) + c.recorder.Event(instance, api.EventTypeWarning, errorWithParameters, s) + return err + } + } + + ns, err := c.kubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) + if err != nil { + s := fmt.Sprintf("Failed to get namespace %q during instance create: %s", instance.Namespace, err) + glog.Info(s) + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + errorFindingNamespaceInstanceReason, + "Error finding namespace for instance. "+s, + ) + c.recorder.Event(instance, api.EventTypeWarning, errorFindingNamespaceInstanceReason, s) + return err + } + + request := &brokerapi.CreateServiceInstanceRequest{ + ServiceID: serviceClass.ExternalID, + PlanID: servicePlan.ExternalID, + Parameters: parameters, + OrgID: string(ns.UID), + SpaceID: string(ns.UID), + AcceptsIncomplete: true, + } + if c.enableOSBAPIContextProfle { + request.ContextProfile = brokerapi.ContextProfile{ + Platform: brokerapi.ContextProfilePlatformKubernetes, + Namespace: instance.Namespace, + } + } + + glog.V(4).Infof("Provisioning a new Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + response, respCode, err := brokerClient.CreateServiceInstance(instance.Spec.ExternalID, request) + if err != nil { + s := fmt.Sprintf("Error provisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q: %s", instance.Namespace, instance.Name, serviceClass.Name, brokerName, err) + glog.Warning(s) + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + errorProvisionCalledReason, + "Provision call failed. "+s) + c.recorder.Event(instance, api.EventTypeWarning, errorProvisionCalledReason, s) + return err + } + + if response.DashboardURL != "" { + instance.Status.DashboardURL = &response.DashboardURL + } + + // Broker can return either a synchronous or asynchronous + // response, if the response is StatusAccepted it's an async + // and we need to add it to the polling queue. Broker can + // optionally return 'Operation' that will then need to be + // passed back to the broker during polling of last_operation. + if respCode == http.StatusAccepted { + glog.V(5).Infof("Received asynchronous provisioning response for Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) + if response.Operation != "" { + instance.Status.LastOperation = &response.Operation + } + + // Tag this instance as having an ongoing async operation so we can enforce + // no other operations against it can start. + instance.Status.AsyncOpInProgress = true + + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + asyncProvisioningReason, + asyncProvisioningMessage, + ) + c.recorder.Eventf(instance, api.EventTypeNormal, asyncProvisioningReason, asyncProvisioningMessage) + + // Actually, start polling this Service Instance by adding it into the polling queue + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(instance) + if err != nil { + glog.Errorf("Couldn't create a key for object %+v: %v", instance, err) + return fmt.Errorf("Couldn't create a key for object %+v: %v", instance, err) + } + c.pollingQueue.Add(key) + } else { + glog.V(5).Infof("Successfully provisioned Instance %v/%v of ServiceClass %v at Broker %v: response: %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName, response) + + // TODO: process response + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionTrue, + successProvisionReason, + successProvisionMessage, + ) + c.recorder.Eventf(instance, api.EventTypeNormal, successProvisionReason, successProvisionMessage) + } + return nil +} + +func (c *controller) pollInstanceInternal(instance *v1alpha1.Instance) error { + glog.V(4).Infof("Processing Instance %v/%v", instance.Namespace, instance.Name) + + serviceClass, servicePlan, brokerName, brokerClient, err := c.getServiceClassPlanAndBroker(instance) + if err != nil { + return err + } + return c.pollInstance(serviceClass, servicePlan, brokerName, brokerClient, instance) +} + +func (c *controller) pollInstance(serviceClass *v1alpha1.ServiceClass, servicePlan *v1alpha1.ServicePlan, brokerName string, brokerClient brokerapi.BrokerClient, instance *v1alpha1.Instance) error { + + // There are some conditions that are different if we're + // deleting, this is more readable than checking the + // timestamps in various places. + deleting := false + if instance.DeletionTimestamp != nil { + deleting = true + } + + lastOperationRequest := &brokerapi.LastOperationRequest{ + ServiceID: serviceClass.ExternalID, + PlanID: servicePlan.ExternalID, + } + if instance.Status.LastOperation != nil && *instance.Status.LastOperation != "" { + lastOperationRequest.Operation = *instance.Status.LastOperation + } + resp, rc, err := brokerClient.PollServiceInstance(instance.Spec.ExternalID, lastOperationRequest) + if err != nil { + glog.Warningf("Poll failed for %v/%v : %s", instance.Namespace, instance.Name, err) + return err + } + glog.V(4).Infof("Poll for %v/%v returned %q : %q", instance.Namespace, instance.Name, resp.State, resp.Description) + + // If the operation was for delete and we receive a http.StatusGone, + // this is considered a success as per the spec, so mark as deleted + // and remove any finalizers. + if rc == http.StatusGone && deleting { + instance.Status.AsyncOpInProgress = false + // Clear the finalizer + if finalizers := sets.NewString(instance.Finalizers...); finalizers.Has(v1alpha1.FinalizerServiceCatalog) { + finalizers.Delete(v1alpha1.FinalizerServiceCatalog) + c.updateInstanceFinalizers(instance, finalizers.List()) + } + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + successDeprovisionReason, + successDeprovisionMessage, + ) + c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) + glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + return nil + } + + switch resp.State { + case "in progress": + // The way the worker keeps on requeueing is by returning an error, so + // we need to keep on polling. + // TODO(vaikas): Update the instance condition with progress message here? + return fmt.Errorf("last operation not completed (still in progress) for %v/%v", instance.Namespace, instance.Name) + case "succeeded": + // this gets updated as a side effect in both cases below. + instance.Status.AsyncOpInProgress = false + + // If we were asynchronously deleting a Service Instance, finish + // the finalizers. + if deleting { + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionFalse, + successDeprovisionReason, + successDeprovisionMessage, + ) + // Clear the finalizer + if finalizers := sets.NewString(instance.Finalizers...); finalizers.Has(v1alpha1.FinalizerServiceCatalog) { + finalizers.Delete(v1alpha1.FinalizerServiceCatalog) + c.updateInstanceFinalizers(instance, finalizers.List()) + } + c.recorder.Event(instance, api.EventTypeNormal, successDeprovisionReason, successDeprovisionMessage) + glog.V(5).Infof("Successfully deprovisioned Instance %v/%v of ServiceClass %v at Broker %v", instance.Namespace, instance.Name, serviceClass.Name, brokerName) + } else { + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + v1alpha1.ConditionTrue, + successProvisionReason, + successProvisionMessage, + ) + } + case "failed": + s := fmt.Sprintf("Error deprovisioning Instance \"%s/%s\" of ServiceClass %q at Broker %q: %q", instance.Namespace, instance.Name, serviceClass.Name, brokerName, resp.Description) + instance.Status.AsyncOpInProgress = false + cond := v1alpha1.ConditionFalse + reason := errorProvisionCalledReason + msg := "Provision call failed: " + s + if deleting { + cond = v1alpha1.ConditionUnknown + reason = errorDeprovisionCalledReason + msg = "Deprovision call failed:" + s + } + c.updateInstanceCondition( + instance, + v1alpha1.InstanceConditionReady, + cond, + reason, + msg, + ) + c.recorder.Event(instance, api.EventTypeWarning, errorDeprovisionCalledReason, s) + default: + glog.Warningf("Got invalid state in LastOperationResponse: %q", resp.State) + return fmt.Errorf("Got invalid state in LastOperationResponse: %q", resp.State) + } + return nil +} + +func findServicePlan(name string, plans []v1alpha1.ServicePlan) *v1alpha1.ServicePlan { + for _, plan := range plans { + if name == plan.Name { + return &plan + } + } + + return nil +} + +// updateInstanceCondition updates the given condition for the given Instance +// with the given status, reason, and message. +func (c *controller) updateInstanceCondition( + instance *v1alpha1.Instance, + conditionType v1alpha1.InstanceConditionType, + status v1alpha1.ConditionStatus, + reason, message string) error { + + clone, err := api.Scheme.DeepCopy(instance) + if err != nil { + return err + } + toUpdate := clone.(*v1alpha1.Instance) + + newCondition := v1alpha1.InstanceCondition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + } + + t := time.Now() + + if len(instance.Status.Conditions) == 0 { + glog.Infof(`Setting lastTransitionTime for Instance "%v/%v" condition %q to %v`, instance.Namespace, instance.Name, conditionType, t) + newCondition.LastTransitionTime = metav1.NewTime(t) + toUpdate.Status.Conditions = []v1alpha1.InstanceCondition{newCondition} + } else { + for i, cond := range instance.Status.Conditions { + if cond.Type == conditionType { + if cond.Status != newCondition.Status { + glog.Infof(`Found status change for Instance "%v/%v" condition %q: %q -> %q; setting lastTransitionTime to %v`, instance.Namespace, instance.Name, conditionType, cond.Status, status, t) + newCondition.LastTransitionTime = metav1.NewTime(t) + } else { + newCondition.LastTransitionTime = cond.LastTransitionTime + } + + toUpdate.Status.Conditions[i] = newCondition + break + } + } + } + + glog.V(4).Infof("Updating %v condition for Instance %v/%v to %v", conditionType, instance.Namespace, instance.Name, status) + _, err = c.serviceCatalogClient.Instances(instance.Namespace).UpdateStatus(toUpdate) + if err != nil { + glog.Errorf("Failed to update condition %v for Instance %v/%v to true: %v", conditionType, instance.Namespace, instance.Name, err) + } + + return err +} + +// updateInstanceFinalizers updates the given finalizers for the given Binding. +func (c *controller) updateInstanceFinalizers( + instance *v1alpha1.Instance, + finalizers []string) error { + + // Get the latest version of the instance so that we can avoid conflicts + // (since we have probably just updated the status of the instance and are + // now removing the last finalizer). + instance, err := c.serviceCatalogClient.Instances(instance.Namespace).Get(instance.Name, metav1.GetOptions{}) + if err != nil { + glog.Errorf("Error getting Instance %v/%v to finalize: %v", instance.Namespace, instance.Name, err) + } + + clone, err := api.Scheme.DeepCopy(instance) + if err != nil { + return err + } + toUpdate := clone.(*v1alpha1.Instance) + + toUpdate.Finalizers = finalizers + + logContext := fmt.Sprintf("finalizers for Instance %v/%v to %v", + instance.Namespace, instance.Name, finalizers) + + glog.V(4).Infof("Updating %v", logContext) + _, err = c.serviceCatalogClient.Instances(instance.Namespace).UpdateStatus(toUpdate) + if err != nil { + glog.Errorf("Error updating %v: %v", logContext, err) + } + return err +} + +func (c *controller) instanceDelete(obj interface{}) { + instance, ok := obj.(*v1alpha1.Instance) + if instance == nil || !ok { + return + } + + glog.V(4).Infof("Received delete event for Instance %v/%v", instance.Namespace, instance.Name) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_instance_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_instance_test.go new file mode 100644 index 000000000000..1d812375667f --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_instance_test.go @@ -0,0 +1,1124 @@ +/* +Copyright 2017 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 controller + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "strings" + "testing" + "time" + + checksum "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/checksum/versioned/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/client-go/pkg/api" + "k8s.io/client-go/pkg/api/v1" + clientgotesting "k8s.io/client-go/testing" +) + +func TestReconcileInstanceNonExistentServiceClass(t *testing.T) { + _, fakeCatalogClient, _, testController, _ := newTestController(t) + + instance := &v1alpha1.Instance{ + ObjectMeta: metav1.ObjectMeta{Name: testInstanceName}, + Spec: v1alpha1.InstanceSpec{ + ServiceClassName: "nothere", + PlanName: "nothere", + ExternalID: instanceGUID, + }, + } + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says it failed because no such class exists. + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance, errorNonexistentServiceClassReason) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorNonexistentServiceClassReason + " " + "Instance \"/test-instance\" references a non-existent ServiceClass \"nothere\"" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceNonExistentBroker(t *testing.T) { + _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) + + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says it failed because no such broker exists. + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance, errorNonexistentBrokerReason) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorNonexistentBrokerReason + " " + "Instance \"test-ns/test-instance\" references a non-existent broker \"test-broker\"" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceWithAuthError(t *testing.T) { + fakeKubeClient, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) + + broker := getTestBroker() + broker.Spec.AuthInfo = &v1alpha1.BrokerAuthInfo{ + BasicAuthSecret: &v1.ObjectReference{ + Namespace: "does_not_exist", + Name: "auth-name", + }, + } + sharedInformers.Brokers().Informer().GetStore().Add(broker) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + + fakeKubeClient.AddReactor("get", "secrets", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("no secret defined") + }) + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updateAction := actions[0].(clientgotesting.UpdateAction) + if e, a := "update", updateAction.GetVerb(); e != a { + t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) + } + updateActionObject := updateAction.GetObject().(*v1alpha1.Instance) + if e, a := testInstanceName, updateActionObject.Name; e != a { + t.Fatalf("Unexpected name of instance created: expected %v, got %v", e, a) + } + if e, a := 1, len(updateActionObject.Status.Conditions); e != a { + t.Fatalf("Unexpected number of conditions: expected %v, got %v", e, a) + } + if e, a := "ErrorGettingAuthCredentials", updateActionObject.Status.Conditions[0].Reason; e != a { + t.Fatalf("Unexpected condition reason: expected %v, got %v", e, a) + } + + // verify one kube action occurred + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 1) + + getAction := kubeActions[0].(clientgotesting.GetAction) + if e, a := "get", getAction.GetVerb(); e != a { + t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) + } + if e, a := "secrets", getAction.GetResource().Resource; e != a { + t.Fatalf("Unexpected resource on action; expected %v, got %v", e, a) + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorAuthCredentialsReason + " " + "Error getting broker auth credentials for broker \"test-broker\": no secret defined" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceNonExistentServicePlan(t *testing.T) { + _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := &v1alpha1.Instance{ + ObjectMeta: metav1.ObjectMeta{Name: testInstanceName}, + Spec: v1alpha1.InstanceSpec{ + ServiceClassName: testServiceClassName, + PlanName: "nothere", + ExternalID: instanceGUID, + }, + } + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // There should only be one action that says it failed because no such class exists. + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance, errorNonexistentServicePlanReason) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorNonexistentServicePlanReason + " " + "Instance \"/test-instance\" references a non-existent ServicePlan \"nothere\" on ServiceClass \"test-serviceclass\"" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceWithParameters(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + + parameters := instanceParameters{Name: "test-param", Args: make(map[string]string)} + parameters.Args["first"] = "first-arg" + parameters.Args["second"] = "second-arg" + + b, err := json.Marshal(parameters) + if err != nil { + t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) + } + instance.Spec.Parameters = &runtime.RawExtension{Raw: b} + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // verify no kube resources created + // One single action comes from getting namespace uid + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyTrue(t, updatedInstance) + + updateObject, ok := updatedInstance.(*v1alpha1.Instance) + if !ok { + t.Fatalf("couldn't convert to *v1alpha1.Instance") + } + + // Verify parameters are what we'd expect them to be, basically name, map with two values in it. + if len(updateObject.Spec.Parameters.Raw) == 0 { + t.Fatalf("Parameters was unexpectedly empty") + } + if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { + t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") + } else { + if len(si.Parameters) == 0 { + t.Fatalf("Expected parameters but got none") + } + if e, a := "test-param", si.Parameters["name"].(string); e != a { + t.Fatalf("Unexpected name for parameters: expected %v, got %v", e, a) + } + argsMap := si.Parameters["args"].(map[string]interface{}) + if e, a := "first-arg", argsMap["first"].(string); e != a { + t.Fatalf("Unexpected value in parameter map: expected %v, got %v", e, a) + } + if e, a := "second-arg", argsMap["second"].(string); e != a { + t.Fatalf("Unexpected value in parameter map: expected %v, got %v", e, a) + } + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successProvisionReason + " " + "The instance was provisioned successfully" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceWithInvalidParameters(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + parameters := instanceParameters{Name: "test-param", Args: make(map[string]string)} + parameters.Args["first"] = "first-arg" + parameters.Args["second"] = "second-arg" + + b, err := json.Marshal(parameters) + if err != nil { + t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) + } + // corrupt the byte slice to begin with a '!' instead of an opening JSON bracket '{' + b[0] = 0x21 + instance.Spec.Parameters = &runtime.RawExtension{Raw: b} + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // verify no kube resources created + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance) + + if si, notOK := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; notOK { + t.Fatalf("Unexpectedly found created Instance: %+v in fakeInstanceClient after creation", si) + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorWithParameters + " " + "Failed to unmarshal Instance parameters" + if e, a := expectedEvent, events[0]; !strings.Contains(a, e) { // event contains RawExtension, so just compare error message + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceWithProvisionFailure(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + parameters := instanceParameters{Name: "test-param", Args: make(map[string]string)} + parameters.Args["first"] = "first-arg" + parameters.Args["second"] = "second-arg" + + b, err := json.Marshal(parameters) + if err != nil { + t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) + } + instance.Spec.Parameters = &runtime.RawExtension{Raw: b} + + fakeBrokerClient.InstanceClient.CreateErr = errors.New("fake creation failure") + + testController.reconcileInstance(instance) + + // verify no kube resources created + // One single action comes from getting namespace uid + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 1) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance) + + if si, notOK := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; notOK { + t.Fatalf("Unexpectedly found created Instance: %+v in fakeInstanceClient after creation", si) + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorProvisionCalledReason + " " + "Error provisioning Instance \"test-ns/test-instance\" of ServiceClass \"test-serviceclass\" at Broker \"test-broker\": fake creation failure" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstance(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + fakeBrokerClient.InstanceClient.DashboardURL = testDashboardURL + + testNsUID := "test_uid_foo" + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(testNsUID), + }, + }, nil + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + + testController.reconcileInstance(instance) + + // Since synchronous operation, must not make it into the polling queue. + if testController.pollingQueue.Len() != 0 { + t.Fatalf("Expected the polling queue to be empty") + } + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // verify no kube resources created. + // One single action comes from getting namespace uid + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyTrue(t, updatedInstance) + + if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { + t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") + } else { + if len(si.Parameters) > 0 { + t.Fatalf("Unexpected parameters, expected none, got %+v", si.Parameters) + } + + if testNsUID != si.OrganizationGUID { + t.Fatalf("Unexpected OrganizationGUID: expected %q, got %q", testNsUID, si.OrganizationGUID) + } + if testNsUID != si.SpaceGUID { + t.Fatalf("Unexpected SpaceGUID: expected %q, got %q", testNsUID, si.SpaceGUID) + } + + assertInstanceDashboardURL(t, instance, testDashboardURL) + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successProvisionReason + " " + successProvisionMessage + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceAsynchronous(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + fakeBrokerClient.InstanceClient.DashboardURL = testDashboardURL + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID("test_uid_foo"), + }, + }, nil + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusAccepted + // And specify that we want broker to return an operation + fakeBrokerClient.InstanceClient.Operation = testOperation + instance := getTestInstance() + + if testController.pollingQueue.Len() != 0 { + t.Fatalf("Expected the polling queue to be empty") + } + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // verify no kube resources created. + // One single action comes from getting namespace uid + kubeActions := fakeKubeClient.Actions() + if e, a := 1, len(kubeActions); e != a { + t.Fatalf("Unexpected number of actions: expected %v, got %v", e, a) + } + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance) + + if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { + t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") + } else { + if len(si.Parameters) > 0 { + t.Fatalf("Unexpected parameters, expected none, got %+v", si.Parameters) + } + + ns, _ := fakeKubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) + if string(ns.UID) != si.OrganizationGUID { + t.Fatalf("Unexpected OrganizationGUID: expected %q, got %q", string(ns.UID), si.OrganizationGUID) + } + if string(ns.UID) != si.SpaceGUID { + t.Fatalf("Unexpected SpaceGUID: expected %q, got %q", string(ns.UID), si.SpaceGUID) + } + } + + // The item should've been added to the pollingQueue for later processing + if testController.pollingQueue.Len() != 1 { + t.Fatalf("Expected the asynchronous instance to end up in the polling queue") + } + item, _ := testController.pollingQueue.Get() + if item == nil { + t.Fatalf("Did not get back a key from polling queue") + } + key := item.(string) + expectedKey := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name) + if key != expectedKey { + t.Fatalf("got key as %q expected %q", key, expectedKey) + } + assertAsyncOpInProgressTrue(t, updatedInstance) + assertInstanceLastOperation(t, updatedInstance, testOperation) + assertInstanceDashboardURL(t, updatedInstance, testDashboardURL) +} + +func TestReconcileInstanceAsynchronousNoOperation(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID("test_uid_foo"), + }, + }, nil + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusAccepted + instance := getTestInstance() + + if testController.pollingQueue.Len() != 0 { + t.Fatalf("Expected the polling queue to be empty") + } + + testController.reconcileInstance(instance) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + // verify no kube resources created. + // One single action comes from getting namespace uid + kubeActions := fakeKubeClient.Actions() + if e, a := 1, len(kubeActions); e != a { + t.Fatalf("Unexpected number of actions: expected %v, got %v", e, a) + } + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance) + + if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { + t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") + } else { + if len(si.Parameters) > 0 { + t.Fatalf("Unexpected parameters, expected none, got %+v", si.Parameters) + } + + ns, _ := fakeKubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) + if string(ns.UID) != si.OrganizationGUID { + t.Fatalf("Unexpected OrganizationGUID: expected %q, got %q", string(ns.UID), si.OrganizationGUID) + } + if string(ns.UID) != si.SpaceGUID { + t.Fatalf("Unexpected SpaceGUID: expected %q, got %q", string(ns.UID), si.SpaceGUID) + } + } + + // The item should've been added to the pollingQueue for later processing + if testController.pollingQueue.Len() != 1 { + t.Fatalf("Expected the asynchronous instance to end up in the polling queue") + } + item, _ := testController.pollingQueue.Get() + if item == nil { + t.Fatalf("Did not get back a key from polling queue") + } + key := item.(string) + expectedKey := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name) + if key != expectedKey { + t.Fatalf("got key as %q expected %q", key, expectedKey) + } + assertAsyncOpInProgressTrue(t, updatedInstance) + assertInstanceLastOperation(t, updatedInstance, "") +} + +func TestReconcileInstanceNamespaceError(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, &v1.Namespace{}, errors.New("No namespace") + }) + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + + testController.reconcileInstance(instance) + + // verify no kube resources created. + // One single action comes from getting namespace uid + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 1) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + assertUpdateStatus(t, actions[0], instance) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeWarning + " " + errorFindingNamespaceInstanceReason + " " + "Failed to get namespace \"test-ns\" during instance create: No namespace" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +func TestReconcileInstanceDelete(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.InstanceClient.Instances = map[string]*brokerapi.ServiceInstance{ + instanceGUID: {}, + } + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + instance.ObjectMeta.DeletionTimestamp = &metav1.Time{} + instance.ObjectMeta.Finalizers = []string{v1alpha1.FinalizerServiceCatalog} + // we only invoke the broker client to deprovision if we have a checksum set + // as that implies a previous success. + checksum := checksum.InstanceSpecChecksum(instance.Spec) + instance.Status.Checksum = &checksum + + fakeCatalogClient.AddReactor("get", "instances", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, instance, nil + }) + + testController.reconcileInstance(instance) + + // Verify no core kube actions occurred + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + // The three actions should be: + // 0. Updating the ready condition + // 1. Get against the instance + // 2. Removing the finalizer + assertNumberOfActions(t, actions, 3) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyFalse(t, updatedInstance) + + assertGet(t, actions[1], instance) + updatedInstance = assertUpdateStatus(t, actions[2], instance) + assertEmptyFinalizers(t, updatedInstance) + + if _, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; ok { + t.Fatalf("Found the deleted Instance in fakeInstanceClient after deletion") + } + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) + + expectedEvent := api.EventTypeNormal + " " + successDeprovisionReason + " " + "The instance was deprovisioned successfully" + if e, a := expectedEvent, events[0]; e != a { + t.Fatalf("Received unexpected event: %v", a) + } +} + +// TestReconcileInstanceDeleteDoesNotInvokeBroker verfies that if an instance is created that is never +// actually provisioned the instance is able to be deleted and is not blocked by any interaction with +// a broker (since its very likely that a broker never actually existed). +func TestReconcileInstanceDeleteDoesNotInvokeBroker(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.InstanceClient.Instances = map[string]*brokerapi.ServiceInstance{ + instanceGUID: {}, + } + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + instance := getTestInstance() + instance.ObjectMeta.DeletionTimestamp = &metav1.Time{} + instance.ObjectMeta.Finalizers = []string{v1alpha1.FinalizerServiceCatalog} + + fakeCatalogClient.AddReactor("get", "instances", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, instance, nil + }) + + testController.reconcileInstance(instance) + + // Verify no core kube actions occurred + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + // The three actions should be: + // 0. Get against the instance + // 1. Removing the finalizer + assertNumberOfActions(t, actions, 2) + + assertGet(t, actions[0], instance) + updatedInstance := assertUpdateStatus(t, actions[1], instance) + assertEmptyFinalizers(t, updatedInstance) + + if _, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { + t.Fatalf("The broker should never have been invoked as service was never provisioned prior.") + } + + // no events because no external deprovision was needed + events := getRecordedEvents(testController) + assertNumEvents(t, events, 0) +} + +func TestPollServiceInstanceInProgressProvisioningWithOperation(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "in progress"} + + instance := getTestInstanceAsyncProvisioning(testOperation) + + err := testController.pollInstanceInternal(instance) + if err == nil { + t.Fatalf("Expected pollInstanceInternal to fail while in progress") + } + // Make sure we get an error which means it will get requeued. + if !strings.Contains(err.Error(), "still in progress") { + t.Fatalf("pollInstanceInternal failed but not with expected error, expected %q got %q", "still in progress", err) + } + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 0) + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) +} + +func TestPollServiceInstanceSuccessProvisioningWithOperation(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} + + instance := getTestInstanceAsyncProvisioning(testOperation) + + err := testController.pollInstanceInternal(instance) + if err != nil { + t.Fatalf("pollInstanceInternal failed: %s", err) + } + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + // Instance should be ready and there no longer is an async operation + // in place. + assertInstanceReadyTrue(t, updatedInstance) + assertAsyncOpInProgressFalse(t, updatedInstance) +} + +func TestPollServiceInstanceFailureProvisioningWithOperation(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "failed"} + + instance := getTestInstanceAsyncProvisioning(testOperation) + + err := testController.pollInstanceInternal(instance) + if err != nil { + t.Fatalf("pollInstanceInternal failed: %s", err) + } + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + // Instance should be not ready and there no longer is an async operation + // in place. + assertInstanceReadyFalse(t, updatedInstance) + assertAsyncOpInProgressFalse(t, updatedInstance) +} + +func TestPollServiceInstanceInProgressDeprovisioningWithOperationNoFinalizer(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "in progress"} + + instance := getTestInstanceAsyncDeprovisioning(testOperation) + + err := testController.pollInstanceInternal(instance) + if err == nil { + t.Fatalf("Expected pollInstanceInternal to fail while in progress") + } + // Make sure we get an error which means it will get requeued. + if !strings.Contains(err.Error(), "still in progress") { + t.Fatalf("pollInstanceInternal failed but not with expected error, expected %q got %q", "still in progress", err) + } + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 0) + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) +} + +func TestPollServiceInstanceSuccessDeprovisioningWithOperationNoFinalizer(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} + + instance := getTestInstanceAsyncDeprovisioning(testOperation) + + err := testController.pollInstanceInternal(instance) + if err != nil { + t.Fatalf("pollInstanceInternal failed: %s", err) + } + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + // Instance should have been deprovisioned + assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionFalse, successDeprovisionReason) + assertAsyncOpInProgressFalse(t, updatedInstance) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) +} + +func TestPollServiceInstanceFailureDeprovisioningWithOperation(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "failed"} + + instance := getTestInstanceAsyncDeprovisioning(testOperation) + + err := testController.pollInstanceInternal(instance) + if err != nil { + t.Fatalf("pollInstanceInternal failed: %s", err) + } + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + // Instance should be set to unknown since the operation on the broker + // failed. + assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionUnknown, errorDeprovisionCalledReason) + assertAsyncOpInProgressFalse(t, updatedInstance) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) +} + +func TestPollServiceInstanceStatusGoneDeprovisioningWithOperationNoFinalizer(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusGone + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} + + instance := getTestInstanceAsyncDeprovisioning(testOperation) + + err := testController.pollInstanceInternal(instance) + if err != nil { + t.Fatalf("pollInstanceInternal failed: %s", err) + } + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + assertNumberOfActions(t, actions, 1) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + // Instance should have been deprovisioned + assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionFalse, successDeprovisionReason) + assertAsyncOpInProgressFalse(t, updatedInstance) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) +} + +func TestPollServiceInstanceSuccessDeprovisioningWithOperationWithFinalizer(t *testing.T) { + fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) + + fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() + + sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) + sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) + + // Specify we want asynchronous provisioning... + fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK + fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} + + instance := getTestInstanceAsyncDeprovisioningWithFinalizer(testOperation) + // updateInstanceFinalizers fetches the latest object. + fakeCatalogClient.AddReactor("get", "instances", func(action clientgotesting.Action) (bool, runtime.Object, error) { + return true, instance, nil + }) + + err := testController.pollInstanceInternal(instance) + if err != nil { + t.Fatalf("pollInstanceInternal failed: %s", err) + } + + // verify no kube resources created. + // No actions + kubeActions := fakeKubeClient.Actions() + assertNumberOfActions(t, kubeActions, 0) + + actions := fakeCatalogClient.Actions() + // The three actions should be: + // 0. Updating the ready condition + // 1. Get against the instance (updateFinalizers calls) + // 2. Removing the finalizer + assertNumberOfActions(t, actions, 3) + + updatedInstance := assertUpdateStatus(t, actions[0], instance) + assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionFalse, successDeprovisionReason) + + // Instance should have been deprovisioned + assertGet(t, actions[1], instance) + updatedInstance = assertUpdateStatus(t, actions[2], instance) + assertEmptyFinalizers(t, updatedInstance) + + events := getRecordedEvents(testController) + assertNumEvents(t, events, 1) +} + +func TestUpdateInstanceCondition(t *testing.T) { + getTestInstanceWithStatus := func(status v1alpha1.ConditionStatus) *v1alpha1.Instance { + instance := getTestInstance() + instance.Status = v1alpha1.InstanceStatus{ + Conditions: []v1alpha1.InstanceCondition{{ + Type: v1alpha1.InstanceConditionReady, + Status: status, + Message: "message", + LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), + }}, + } + + return instance + } + + cases := []struct { + name string + input *v1alpha1.Instance + status v1alpha1.ConditionStatus + reason string + message string + transitionTimeChanged bool + }{ + + { + name: "initially unset", + input: getTestInstance(), + status: v1alpha1.ConditionFalse, + message: "message", + transitionTimeChanged: true, + }, + { + name: "not ready -> not ready", + input: getTestInstanceWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionFalse, + transitionTimeChanged: false, + }, + { + name: "not ready -> not ready, reason and message change", + input: getTestInstanceWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionFalse, + reason: "foo", + message: "bar", + transitionTimeChanged: false, + }, + { + name: "not ready -> ready", + input: getTestInstanceWithStatus(v1alpha1.ConditionFalse), + status: v1alpha1.ConditionTrue, + message: "message", + transitionTimeChanged: true, + }, + { + name: "ready -> ready", + input: getTestInstanceWithStatus(v1alpha1.ConditionTrue), + status: v1alpha1.ConditionTrue, + message: "message", + transitionTimeChanged: false, + }, + { + name: "ready -> not ready", + input: getTestInstanceWithStatus(v1alpha1.ConditionTrue), + status: v1alpha1.ConditionFalse, + message: "message", + transitionTimeChanged: true, + }, + { + name: "message -> message2", + input: getTestInstanceWithStatus(v1alpha1.ConditionTrue), + status: v1alpha1.ConditionFalse, + message: "message2", + transitionTimeChanged: true, + }, + } + + for _, tc := range cases { + _, fakeCatalogClient, _, testController, _ := newTestController(t) + + clone, err := api.Scheme.DeepCopy(tc.input) + if err != nil { + t.Errorf("%v: deep copy failed", tc.name) + continue + } + inputClone := clone.(*v1alpha1.Instance) + + err = testController.updateInstanceCondition(tc.input, v1alpha1.InstanceConditionReady, tc.status, tc.reason, tc.message) + if err != nil { + t.Errorf("%v: error updating instance condition: %v", tc.name, err) + continue + } + + if !reflect.DeepEqual(tc.input, inputClone) { + t.Errorf("%v: updating broker condition mutated input: expected %v, got %v", tc.name, inputClone, tc.input) + continue + } + + actions := fakeCatalogClient.Actions() + if ok := expectNumberOfActions(t, tc.name, actions, 1); !ok { + continue + } + + updatedInstance, ok := expectUpdateStatus(t, tc.name, actions[0], tc.input) + if !ok { + continue + } + + updateActionObject, ok := updatedInstance.(*v1alpha1.Instance) + if !ok { + t.Errorf("%v: couldn't convert to instance", tc.name) + continue + } + + var initialTs metav1.Time + if len(inputClone.Status.Conditions) != 0 { + initialTs = inputClone.Status.Conditions[0].LastTransitionTime + } + + if e, a := 1, len(updateActionObject.Status.Conditions); e != a { + t.Errorf("%v: expected %v condition(s), got %v", tc.name, e, a) + } + + outputCondition := updateActionObject.Status.Conditions[0] + newTs := outputCondition.LastTransitionTime + + if tc.transitionTimeChanged && initialTs == newTs { + t.Errorf("%v: transition time didn't change when it should have", tc.name) + continue + } else if !tc.transitionTimeChanged && initialTs != newTs { + t.Errorf("%v: transition time changed when it shouldn't have", tc.name) + continue + } + if e, a := tc.reason, outputCondition.Reason; e != "" && e != a { + t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) + continue + } + if e, a := tc.message, outputCondition.Message; e != "" && e != a { + t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) + } + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_serviceclass.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_serviceclass.go new file mode 100644 index 000000000000..693ad8a02470 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_serviceclass.go @@ -0,0 +1,67 @@ +/* +Copyright 2017 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 controller + +import ( + "github.com/golang/glog" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/cache" +) + +// Service class handlers and control-loop + +func (c *controller) serviceClassAdd(obj interface{}) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + glog.Errorf("Couldn't get key for object %+v: %v", obj, err) + return + } + c.serviceClassQueue.Add(key) +} + +func (c *controller) reconcileServiceClassKey(key string) error { + serviceClass, err := c.serviceClassLister.Get(key) + if errors.IsNotFound(err) { + glog.Infof("Not doing work for ServiceClass %v because it has been deleted", key) + return nil + } + if err != nil { + glog.Errorf("Unable to retrieve ServiceClass %v from store: %v", key, err) + return err + } + + return c.reconcileServiceClass(serviceClass) +} + +func (c *controller) reconcileServiceClass(serviceClass *v1alpha1.ServiceClass) error { + glog.V(4).Infof("Processing ServiceClass %v", serviceClass.Name) + return nil +} + +func (c *controller) serviceClassUpdate(oldObj, newObj interface{}) { + c.serviceClassAdd(newObj) +} + +func (c *controller) serviceClassDelete(obj interface{}) { + serviceClass, ok := obj.(*v1alpha1.ServiceClass) + if serviceClass == nil || !ok { + return + } + + glog.V(4).Infof("Received delete event for ServiceClass %v", serviceClass.Name) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_test.go index d79e01536590..d9075c6c4a44 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/controller/controller_test.go @@ -18,12 +18,9 @@ package controller import ( "encoding/json" - "errors" - "fmt" - "net/http" + "net/http/httptest" "reflect" "runtime/debug" - "strings" "testing" "time" @@ -36,16 +33,25 @@ import ( servicecatalogclientset "github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/clientset/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" + fakebrokerserver "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake/server" clientgofake "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/pkg/api" "k8s.io/client-go/pkg/api/v1" clientgotesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/record" ) +// NOTE: +// +// This file contains: +// +// - tests for the methods on controller.go +// - test fixtures used in other controller_*_test.go files +// +// Other controller_*_test.go files contain tests related to the reconcilation +// loops for the different catalog API resources. + const ( serviceClassGUID = "SCGUID" planGUID = "PGUID" @@ -189,6 +195,131 @@ const testCatalogWithMultipleServices = `{ } ]}` +const alphaParameterSchemaCatalogBytes = `{ + "services": [{ + "name": "fake-service", + "id": "acb56d7c-XXXX-XXXX-XXXX-feb140a59a66", + "description": "fake service", + "tags": ["tag1", "tag2"], + "requires": ["route_forwarding"], + "bindable": true, + "metadata": { + "a": "b", + "c": "d" + }, + "dashboard_client": { + "id": "398e2f8e-XXXX-XXXX-XXXX-19a71ecbcf64", + "secret": "277cabb0-XXXX-XXXX-XXXX-7822c0a90e5d", + "redirect_uri": "http://localhost:1234" + }, + "plan_updateable": true, + "plans": [{ + "name": "fake-plan-1", + "id": "d3031751-XXXX-XXXX-XXXX-a42377d3320e", + "description": "description1", + "metadata": { + "b": "c", + "d": "e" + }, + "schemas": { + "service_instance": { + "create": { + "parameters": { + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "title": "Parameters", + "properties": { + "name": { + "title": "Queue Name", + "type": "string", + "maxLength": 63, + "default": "My Queue" + }, + "email": { + "title": "Email", + "type": "string", + "pattern": "^\\S+@\\S+$", + "description": "Email address for alerts." + }, + "protocol": { + "title": "Protocol", + "type": "string", + "default": "Java Message Service (JMS) 1.1", + "enum": [ + "Java Message Service (JMS) 1.1", + "Transmission Control Protocol (TCP)", + "Advanced Message Queuing Protocol (AMQP) 1.0" + ] + }, + "secure": { + "title": "Enable security", + "type": "boolean", + "default": true + } + }, + "required": [ + "name", + "protocol" + ] + } + }, + "update": { + "parameters": { + "baz": "zap" + } + } + }, + "service_binding": { + "create": { + "parameters": { + "zoo": "blu" + } + } + } + } + }] + }] +}` + +const instanceParameterSchemaBytes = `{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "title": "Parameters", + "properties": { + "name": { + "title": "Queue Name", + "type": "string", + "maxLength": 63, + "default": "My Queue" + }, + "email": { + "title": "Email", + "type": "string", + "pattern": "^\\S+@\\S+$", + "description": "Email address for alerts." + }, + "protocol": { + "title": "Protocol", + "type": "string", + "default": "Java Message Service (JMS) 1.1", + "enum": [ + "Java Message Service (JMS) 1.1", + "Transmission Control Protocol (TCP)", + "Advanced Message Queuing Protocol (AMQP) 1.0" + ] + }, + "secure": { + "title": "Enable security", + "type": "boolean", + "default": true + } + }, + "required": [ + "name", + "protocol" + ] +}` + // broker used in most of the tests that need a broker func getTestBroker() *v1alpha1.Broker { return &v1alpha1.Broker{ @@ -253,2267 +384,148 @@ func getTestNonbindableServiceClass() *v1alpha1.ServiceClass { Name: testNonbindablePlanName, Free: true, ExternalID: nonbindablePlanGUID, - }, - }, - } -} - -// broker catalog that provides the service class named in of -// getTestServiceClass() -func getTestCatalog() *brokerapi.Catalog { - return &brokerapi.Catalog{ - Services: []*brokerapi.Service{ - { - Name: testServiceClassName, - ID: serviceClassGUID, - Description: "a test service", - Bindable: true, - Plans: []brokerapi.ServicePlan{ - { - Name: testPlanName, - Free: true, - ID: planGUID, - Description: "a test plan", - }, - }, - }, - }, - } -} - -// instance referencing the result of getTestServiceClass() -func getTestInstance() *v1alpha1.Instance { - return &v1alpha1.Instance{ - ObjectMeta: metav1.ObjectMeta{Name: testInstanceName, Namespace: testNamespace}, - Spec: v1alpha1.InstanceSpec{ - ServiceClassName: testServiceClassName, - PlanName: testPlanName, - ExternalID: instanceGUID, - }, - } -} - -// an instance referencing the result of getTestNonbindableServiceClass, on the non-bindable plan. -func getTestNonbindableInstance() *v1alpha1.Instance { - i := getTestInstance() - i.Spec.ServiceClassName = testNonbindableServiceClassName - i.Spec.PlanName = testNonbindablePlanName - - return i -} - -// an instance referencing the result of getTestNonbindableServiceClass, on the bindable plan. -func getTestInstanceNonbindableServiceBindablePlan() *v1alpha1.Instance { - i := getTestNonbindableInstance() - i.Spec.PlanName = testPlanName - - return i -} - -func getTestInstanceBindableServiceNonbindablePlan() *v1alpha1.Instance { - i := getTestInstance() - i.Spec.PlanName = testNonbindablePlanName - - return i -} - -func getTestInstanceWithStatus(status v1alpha1.ConditionStatus) *v1alpha1.Instance { - instance := getTestInstance() - instance.Status = v1alpha1.InstanceStatus{ - Conditions: []v1alpha1.InstanceCondition{{ - Type: v1alpha1.InstanceConditionReady, - Status: status, - LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), - }}, - } - - return instance -} - -// getTestInstanceAsync returns an instance in async mode -func getTestInstanceAsyncProvisioning(operation string) *v1alpha1.Instance { - instance := getTestInstance() - if operation != "" { - instance.Status.LastOperation = &operation - } - instance.Status = v1alpha1.InstanceStatus{ - Conditions: []v1alpha1.InstanceCondition{{ - Type: v1alpha1.InstanceConditionReady, - Status: v1alpha1.ConditionFalse, - Message: "Provisioning", - LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), - }}, - AsyncOpInProgress: true, - } - - return instance -} - -func getTestInstanceAsyncDeprovisioning(operation string) *v1alpha1.Instance { - instance := getTestInstance() - if operation != "" { - instance.Status.LastOperation = &operation - } - instance.Status = v1alpha1.InstanceStatus{ - Conditions: []v1alpha1.InstanceCondition{{ - Type: v1alpha1.InstanceConditionReady, - Status: v1alpha1.ConditionFalse, - Message: "Deprovisioning", - LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), - }}, - AsyncOpInProgress: true, - } - - // Set the deleted timestamp to simulate deletion - ts := metav1.NewTime(time.Now().Add(-5 * time.Minute)) - instance.DeletionTimestamp = &ts - return instance -} - -func getTestInstanceAsyncDeprovisioningWithFinalizer(operation string) *v1alpha1.Instance { - instance := getTestInstanceAsyncDeprovisioning(operation) - instance.ObjectMeta.Finalizers = []string{"kubernetes"} - return instance -} - -// binding referencing the result of getTestBinding() -func getTestBinding() *v1alpha1.Binding { - return &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - }, - } - -} - -type instanceParameters struct { - Name string `json:"name"` - Args map[string]string `json:"args"` -} - -type bindingParameters struct { - Name string `json:"name"` - Args []string `json:"args"` -} - -func TestShouldReconcileBroker(t *testing.T) { - cases := []struct { - name string - broker *v1alpha1.Broker - now time.Time - interval time.Duration - reconcile bool - }{ - { - name: "no status", - broker: getTestBroker(), - now: time.Now(), - interval: 3 * time.Minute, - reconcile: true, - }, - { - name: "no ready condition", - broker: func() *v1alpha1.Broker { - b := getTestBroker() - b.Status = v1alpha1.BrokerStatus{ - Conditions: []v1alpha1.BrokerCondition{ - { - Type: v1alpha1.BrokerConditionType("NotARealCondition"), - Status: v1alpha1.ConditionTrue, - }, - }, - } - return b - }(), - now: time.Now(), - interval: 3 * time.Minute, - reconcile: true, - }, - { - name: "not ready", - broker: getTestBrokerWithStatus(v1alpha1.ConditionFalse), - now: time.Now(), - interval: 3 * time.Minute, - reconcile: true, - }, - { - name: "ready, interval elapsed", - broker: func() *v1alpha1.Broker { - broker := getTestBrokerWithStatus(v1alpha1.ConditionTrue) - return broker - }(), - now: time.Now(), - interval: 3 * time.Minute, - reconcile: true, - }, - { - name: "ready, interval not elapsed", - broker: func() *v1alpha1.Broker { - broker := getTestBrokerWithStatus(v1alpha1.ConditionTrue) - return broker - }(), - now: time.Now(), - interval: 3 * time.Hour, - reconcile: false, - }, - } - - for _, tc := range cases { - var ltt *time.Time - if len(tc.broker.Status.Conditions) != 0 { - ltt = &tc.broker.Status.Conditions[0].LastTransitionTime.Time - } - - t.Logf("%v: now: %v, interval: %v, last transition time: %v", tc.name, tc.now, tc.interval, ltt) - actual := shouldReconcileBroker(tc.broker, tc.now, tc.interval) - - if e, a := tc.reconcile, actual; e != a { - t.Errorf("%v: unexpected result: expected %v, got %v", tc.name, e, a) - } - } -} - -func TestReconcileBroker(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, _ := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - testController.reconcileBroker(getTestBroker()) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 2) - - // first action should be a create action for a service class - assertCreate(t, actions[0], getTestServiceClass()) - - // second action should be an update action for broker status subresource - updatedBroker := assertUpdateStatus(t, actions[1], getTestBroker()) - assertBrokerReadyTrue(t, updatedBroker) - - // verify no kube resources created - assertNumberOfActions(t, fakeKubeClient.Actions(), 0) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeNormal + " " + successFetchedCatalogReason + " " + successFetchedCatalogMessage - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBrokerExistingServiceClass(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - testServiceClass := getTestServiceClass() - sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - testController.reconcileBroker(getTestBroker()) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 2) - - // first action should be an update action for a service class - assertUpdate(t, actions[0], testServiceClass) - - // second action should be an update action for broker status subresource - updatedBroker := assertUpdateStatus(t, actions[1], getTestBroker()) - assertBrokerReadyTrue(t, updatedBroker) - - // verify no kube resources created - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) -} - -func TestReconcileBrokerExistingServiceClassDifferentExternalID(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - testServiceClass := getTestServiceClass() - testServiceClass.ExternalID = "notTheSame" - sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - testController.reconcileBroker(getTestBroker()) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedBroker := assertUpdateStatus(t, actions[0], getTestBroker()) - assertBrokerReadyFalse(t, updatedBroker) - - // verify no kube resources created - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorSyncingCatalogReason + ` Error reconciling serviceClass "test-serviceclass" (broker "test-broker"): ServiceClass "test-serviceclass" already exists with OSB guid "notTheSame", received different guid "SCGUID"` - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event; expected\n%v, got\n%v", e, a) - } -} - -func TestReconcileBrokerExistingServiceClassDifferentBroker(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - testServiceClass := getTestServiceClass() - testServiceClass.BrokerName = "notTheSame" - sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - testController.reconcileBroker(getTestBroker()) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedBroker := assertUpdateStatus(t, actions[0], getTestBroker()) - assertBrokerReadyFalse(t, updatedBroker) - - // verify no kube resources created - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorSyncingCatalogReason + ` Error reconciling serviceClass "test-serviceclass" (broker "test-broker"): ServiceClass "test-serviceclass" for Broker "test-broker" already exists for Broker "notTheSame"` - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event; expected\n%v, got\n%v", e, a) - } -} - -func TestReconcileBrokerDelete(t *testing.T) { - fakeKubeClient, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) - - testServiceClass := getTestServiceClass() - sharedInformers.ServiceClasses().Informer().GetStore().Add(testServiceClass) - - broker := getTestBroker() - broker.DeletionTimestamp = &metav1.Time{} - broker.Finalizers = []string{"kubernetes"} - - testController.reconcileBroker(broker) - - // Verify no core kube actions occurred - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - // The three actions should be: - // 0. Deleting the associated ServiceClass - // 1. Updating the ready condition - // 2. Removing the finalizer - assertNumberOfActions(t, actions, 3) - - assertDelete(t, actions[0], testServiceClass) - - updatedBroker := assertUpdateStatus(t, actions[1], broker) - assertBrokerReadyFalse(t, updatedBroker) - - updatedBroker = assertUpdateStatus(t, actions[2], broker) - assertEmptyFinalizers(t, updatedBroker) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeNormal + " " + successBrokerDeletedReason + " " + "The broker test-broker was deleted successfully." - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBrokerErrorFetchingCatalog(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, _ := newTestController(t) - - fakeBrokerClient.CatalogClient.RetErr = fakebrokerapi.ErrInstanceNotFound - broker := getTestBroker() - - testController.reconcileBroker(broker) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedBroker := assertUpdateStatus(t, actions[0], broker) - assertBrokerReadyFalse(t, updatedBroker) - - assertNumberOfActions(t, fakeKubeClient.Actions(), 0) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorFetchingCatalogReason + " " + "Error getting broker catalog for broker \"test-broker\": instance not found" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBrokerWithAuthError(t *testing.T) { - fakeKubeClient, fakeCatalogClient, _, testController, _ := newTestController(t) - - broker := getTestBroker() - broker.Spec.AuthSecret = &v1.ObjectReference{ - Namespace: "does_not_exist", - Name: "auth-name", - } - - fakeKubeClient.AddReactor("get", "secrets", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("no secret defined") - }) - - testController.reconcileBroker(broker) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedBroker := assertUpdateStatus(t, actions[0], broker) - assertBrokerReadyFalse(t, updatedBroker) - - // verify one kube action occurred - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 1) - - getAction := kubeActions[0].(clientgotesting.GetAction) - if e, a := "get", getAction.GetVerb(); e != a { - t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) - } - if e, a := "secrets", getAction.GetResource().Resource; e != a { - t.Fatalf("Unexpected resource on action; expected %v, got %v", e, a) - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorAuthCredentialsReason + " " + "Error getting broker auth credentials for broker \"test-broker\": no secret defined" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBrokerWithReconcileError(t *testing.T) { - fakeKubeClient, fakeCatalogClient, _, testController, _ := newTestController(t) - - broker := getTestBroker() - broker.Spec.AuthSecret = &v1.ObjectReference{ - Namespace: "does_not_exist", - Name: "auth-name", - } - - fakeCatalogClient.AddReactor("create", "serviceclasses", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("error creating serviceclass") - }) - - testController.reconcileBroker(broker) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedBroker := assertUpdateStatus(t, actions[0], broker) - assertBrokerReadyFalse(t, updatedBroker) - - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 1) - - getAction := kubeActions[0].(clientgotesting.GetAction) - if e, a := "get", getAction.GetVerb(); e != a { - t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorAuthCredentialsReason + " " + "Error getting broker auth credentials for broker \"test-broker\": auth secret didn't contain username" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestUpdateBrokerCondition(t *testing.T) { - cases := []struct { - name string - input *v1alpha1.Broker - status v1alpha1.ConditionStatus - reason string - message string - transitionTimeChanged bool - }{ - - { - name: "initially unset", - input: getTestBroker(), - status: v1alpha1.ConditionFalse, - transitionTimeChanged: true, - }, - { - name: "not ready -> not ready", - input: getTestBrokerWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionFalse, - transitionTimeChanged: false, - }, - { - name: "not ready -> not ready with reason and message change", - input: getTestBrokerWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionFalse, - reason: "foo", - message: "bar", - transitionTimeChanged: false, - }, - { - name: "not ready -> ready", - input: getTestBrokerWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionTrue, - transitionTimeChanged: true, - }, - { - name: "ready -> ready", - input: getTestBrokerWithStatus(v1alpha1.ConditionTrue), - status: v1alpha1.ConditionTrue, - transitionTimeChanged: false, - }, - { - name: "ready -> not ready", - input: getTestBrokerWithStatus(v1alpha1.ConditionTrue), - status: v1alpha1.ConditionFalse, - transitionTimeChanged: true, - }, - } - - for _, tc := range cases { - _, fakeCatalogClient, _, testController, _ := newTestController(t) - - clone, err := api.Scheme.DeepCopy(tc.input) - if err != nil { - t.Errorf("%v: deep copy failed", tc.name) - continue - } - - inputClone := clone.(*v1alpha1.Broker) - - err = testController.updateBrokerCondition(tc.input, v1alpha1.BrokerConditionReady, tc.status, tc.reason, tc.message) - if err != nil { - t.Errorf("%v: error updating broker condition: %v", tc.name, err) - continue - } - - if !reflect.DeepEqual(tc.input, inputClone) { - t.Errorf("%v: updating broker condition mutated input: expected %v, got %v", tc.name, inputClone, tc.input) - continue - } - - actions := fakeCatalogClient.Actions() - if ok := expectNumberOfActions(t, tc.name, actions, 1); !ok { - continue - } - - updatedBroker, ok := expectUpdateStatus(t, tc.name, actions[0], tc.input) - if !ok { - continue - } - - updateActionObject, ok := updatedBroker.(*v1alpha1.Broker) - if !ok { - t.Errorf("%v: couldn't convert to broker", tc.name) - continue - } - - var initialTs metav1.Time - if len(inputClone.Status.Conditions) != 0 { - initialTs = inputClone.Status.Conditions[0].LastTransitionTime - } - - if e, a := 1, len(updateActionObject.Status.Conditions); e != a { - t.Errorf("%v: expected %v condition(s), got %v", tc.name, e, a) - } - - outputCondition := updateActionObject.Status.Conditions[0] - newTs := outputCondition.LastTransitionTime - - if tc.transitionTimeChanged && initialTs == newTs { - t.Errorf("%v: transition time didn't change when it should have", tc.name) - continue - } else if !tc.transitionTimeChanged && initialTs != newTs { - t.Errorf("%v: transition time changed when it shouldn't have", tc.name) - continue - } - if e, a := tc.reason, outputCondition.Reason; e != "" && e != a { - t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) - continue - } - if e, a := tc.message, outputCondition.Message; e != "" && e != a { - t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) - } - } -} - -func TestReconcileInstanceNonExistentServiceClass(t *testing.T) { - _, fakeCatalogClient, _, testController, _ := newTestController(t) - - instance := &v1alpha1.Instance{ - ObjectMeta: metav1.ObjectMeta{Name: testInstanceName}, - Spec: v1alpha1.InstanceSpec{ - ServiceClassName: "nothere", - PlanName: "nothere", - ExternalID: instanceGUID, - }, - } - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says it failed because no such class exists. - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance, errorNonexistentServiceClassReason) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorNonexistentServiceClassReason + " " + "Instance \"/test-instance\" references a non-existent ServiceClass \"nothere\"" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceNonExistentBroker(t *testing.T) { - _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) - - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says it failed because no such broker exists. - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance, errorNonexistentBrokerReason) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorNonexistentBrokerReason + " " + "Instance \"test-ns/test-instance\" references a non-existent broker \"test-broker\"" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceWithAuthError(t *testing.T) { - fakeKubeClient, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) - - broker := getTestBroker() - broker.Spec.AuthSecret = &v1.ObjectReference{ - Namespace: "does_not_exist", - Name: "auth-name", - } - sharedInformers.Brokers().Informer().GetStore().Add(broker) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - - fakeKubeClient.AddReactor("get", "secrets", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, nil, errors.New("no secret defined") - }) - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updateAction := actions[0].(clientgotesting.UpdateAction) - if e, a := "update", updateAction.GetVerb(); e != a { - t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) - } - updateActionObject := updateAction.GetObject().(*v1alpha1.Instance) - if e, a := testInstanceName, updateActionObject.Name; e != a { - t.Fatalf("Unexpected name of instance created: expected %v, got %v", e, a) - } - if e, a := 1, len(updateActionObject.Status.Conditions); e != a { - t.Fatalf("Unexpected number of conditions: expected %v, got %v", e, a) - } - if e, a := "ErrorGettingAuthCredentials", updateActionObject.Status.Conditions[0].Reason; e != a { - t.Fatalf("Unexpected condition reason: expected %v, got %v", e, a) - } - - // verify one kube action occurred - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 1) - - getAction := kubeActions[0].(clientgotesting.GetAction) - if e, a := "get", getAction.GetVerb(); e != a { - t.Fatalf("Unexpected verb on action; expected %v, got %v", e, a) - } - if e, a := "secrets", getAction.GetResource().Resource; e != a { - t.Fatalf("Unexpected resource on action; expected %v, got %v", e, a) - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorAuthCredentialsReason + " " + "Error getting broker auth credentials for broker \"test-broker\": no secret defined" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceNonExistentServicePlan(t *testing.T) { - _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := &v1alpha1.Instance{ - ObjectMeta: metav1.ObjectMeta{Name: testInstanceName}, - Spec: v1alpha1.InstanceSpec{ - ServiceClassName: testServiceClassName, - PlanName: "nothere", - ExternalID: instanceGUID, - }, - } - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says it failed because no such class exists. - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance, errorNonexistentServicePlanReason) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorNonexistentServicePlanReason + " " + "Instance \"/test-instance\" references a non-existent ServicePlan \"nothere\" on ServiceClass \"test-serviceclass\"" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceWithParameters(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - - parameters := instanceParameters{Name: "test-param", Args: make(map[string]string)} - parameters.Args["first"] = "first-arg" - parameters.Args["second"] = "second-arg" - - b, err := json.Marshal(parameters) - if err != nil { - t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) - } - instance.Spec.Parameters = &runtime.RawExtension{Raw: b} - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // verify no kube resources created - // One single action comes from getting namespace uid - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyTrue(t, updatedInstance) - - updateObject, ok := updatedInstance.(*v1alpha1.Instance) - if !ok { - t.Fatalf("couldn't convert to *v1alpha1.Instance") - } - - // Verify parameters are what we'd expect them to be, basically name, map with two values in it. - if len(updateObject.Spec.Parameters.Raw) == 0 { - t.Fatalf("Parameters was unexpectedly empty") - } - if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { - t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") - } else { - if len(si.Parameters) == 0 { - t.Fatalf("Expected parameters but got none") - } - if e, a := "test-param", si.Parameters["name"].(string); e != a { - t.Fatalf("Unexpected name for parameters: expected %v, got %v", e, a) - } - argsMap := si.Parameters["args"].(map[string]interface{}) - if e, a := "first-arg", argsMap["first"].(string); e != a { - t.Fatalf("Unexpected value in parameter map: expected %v, got %v", e, a) - } - if e, a := "second-arg", argsMap["second"].(string); e != a { - t.Fatalf("Unexpected value in parameter map: expected %v, got %v", e, a) - } - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeNormal + " " + successProvisionReason + " " + "The instance was provisioned successfully" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceWithInvalidParameters(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - parameters := instanceParameters{Name: "test-param", Args: make(map[string]string)} - parameters.Args["first"] = "first-arg" - parameters.Args["second"] = "second-arg" - - b, err := json.Marshal(parameters) - if err != nil { - t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) - } - // corrupt the byte slice to begin with a '!' instead of an opening JSON bracket '{' - b[0] = 0x21 - instance.Spec.Parameters = &runtime.RawExtension{Raw: b} - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // verify no kube resources created - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance) - - if si, notOK := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; notOK { - t.Fatalf("Unexpectedly found created Instance: %+v in fakeInstanceClient after creation", si) - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorWithParameters + " " + "Failed to unmarshal Instance parameters" - if e, a := expectedEvent, events[0]; !strings.Contains(a, e) { // event contains RawExtension, so just compare error message - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceWithProvisionFailure(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - parameters := instanceParameters{Name: "test-param", Args: make(map[string]string)} - parameters.Args["first"] = "first-arg" - parameters.Args["second"] = "second-arg" - - b, err := json.Marshal(parameters) - if err != nil { - t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) - } - instance.Spec.Parameters = &runtime.RawExtension{Raw: b} - - fakeBrokerClient.InstanceClient.CreateErr = errors.New("fake creation failure") - - testController.reconcileInstance(instance) - - // verify no kube resources created - // One single action comes from getting namespace uid - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 1) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance) - - if si, notOK := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; notOK { - t.Fatalf("Unexpectedly found created Instance: %+v in fakeInstanceClient after creation", si) - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorProvisionCalledReason + " " + "Error provisioning Instance \"test-ns/test-instance\" of ServiceClass \"test-serviceclass\" at Broker \"test-broker\": fake creation failure" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstance(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - fakeBrokerClient.InstanceClient.DashboardURL = testDashboardURL - - testNsUID := "test_uid_foo" - - fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - UID: types.UID(testNsUID), - }, - }, nil - }) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - - testController.reconcileInstance(instance) - - // Since synchronous operation, must not make it into the polling queue. - if testController.pollingQueue.Len() != 0 { - t.Fatalf("Expected the polling queue to be empty") - } - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // verify no kube resources created. - // One single action comes from getting namespace uid - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyTrue(t, updatedInstance) - - if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { - t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") - } else { - if len(si.Parameters) > 0 { - t.Fatalf("Unexpected parameters, expected none, got %+v", si.Parameters) - } - - if testNsUID != si.OrganizationGUID { - t.Fatalf("Unexpected OrganizationGUID: expected %q, got %q", testNsUID, si.OrganizationGUID) - } - if testNsUID != si.SpaceGUID { - t.Fatalf("Unexpected SpaceGUID: expected %q, got %q", testNsUID, si.SpaceGUID) - } - - assertInstanceDashboardURL(t, instance, testDashboardURL) - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeNormal + " " + successProvisionReason + " " + successProvisionMessage - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceAsynchronous(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - fakeBrokerClient.InstanceClient.DashboardURL = testDashboardURL - - fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - UID: types.UID("test_uid_foo"), - }, - }, nil - }) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusAccepted - // And specify that we want broker to return an operation - fakeBrokerClient.InstanceClient.Operation = testOperation - instance := getTestInstance() - - if testController.pollingQueue.Len() != 0 { - t.Fatalf("Expected the polling queue to be empty") - } - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // verify no kube resources created. - // One single action comes from getting namespace uid - kubeActions := fakeKubeClient.Actions() - if e, a := 1, len(kubeActions); e != a { - t.Fatalf("Unexpected number of actions: expected %v, got %v", e, a) - } - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance) - - if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { - t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") - } else { - if len(si.Parameters) > 0 { - t.Fatalf("Unexpected parameters, expected none, got %+v", si.Parameters) - } - - ns, _ := fakeKubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) - if string(ns.UID) != si.OrganizationGUID { - t.Fatalf("Unexpected OrganizationGUID: expected %q, got %q", string(ns.UID), si.OrganizationGUID) - } - if string(ns.UID) != si.SpaceGUID { - t.Fatalf("Unexpected SpaceGUID: expected %q, got %q", string(ns.UID), si.SpaceGUID) - } - } - - // The item should've been added to the pollingQueue for later processing - if testController.pollingQueue.Len() != 1 { - t.Fatalf("Expected the asynchronous instance to end up in the polling queue") - } - item, _ := testController.pollingQueue.Get() - if item == nil { - t.Fatalf("Did not get back a key from polling queue") - } - key := item.(string) - expectedKey := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name) - if key != expectedKey { - t.Fatalf("got key as %q expected %q", key, expectedKey) - } - assertAsyncOpInProgressTrue(t, updatedInstance) - assertInstanceLastOperation(t, updatedInstance, testOperation) - assertInstanceDashboardURL(t, updatedInstance, testDashboardURL) -} - -func TestReconcileInstanceAsynchronousNoOperation(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - UID: types.UID("test_uid_foo"), - }, - }, nil - }) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusAccepted - instance := getTestInstance() - - if testController.pollingQueue.Len() != 0 { - t.Fatalf("Expected the polling queue to be empty") - } - - testController.reconcileInstance(instance) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // verify no kube resources created. - // One single action comes from getting namespace uid - kubeActions := fakeKubeClient.Actions() - if e, a := 1, len(kubeActions); e != a { - t.Fatalf("Unexpected number of actions: expected %v, got %v", e, a) - } - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance) - - if si, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; !ok { - t.Fatalf("Did not find the created Instance in fakeInstanceClient after creation") - } else { - if len(si.Parameters) > 0 { - t.Fatalf("Unexpected parameters, expected none, got %+v", si.Parameters) - } - - ns, _ := fakeKubeClient.Core().Namespaces().Get(instance.Namespace, metav1.GetOptions{}) - if string(ns.UID) != si.OrganizationGUID { - t.Fatalf("Unexpected OrganizationGUID: expected %q, got %q", string(ns.UID), si.OrganizationGUID) - } - if string(ns.UID) != si.SpaceGUID { - t.Fatalf("Unexpected SpaceGUID: expected %q, got %q", string(ns.UID), si.SpaceGUID) - } - } - - // The item should've been added to the pollingQueue for later processing - if testController.pollingQueue.Len() != 1 { - t.Fatalf("Expected the asynchronous instance to end up in the polling queue") - } - item, _ := testController.pollingQueue.Get() - if item == nil { - t.Fatalf("Did not get back a key from polling queue") - } - key := item.(string) - expectedKey := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name) - if key != expectedKey { - t.Fatalf("got key as %q expected %q", key, expectedKey) - } - assertAsyncOpInProgressTrue(t, updatedInstance) - assertInstanceLastOperation(t, updatedInstance, "") -} - -func TestReconcileInstanceNamespaceError(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, &v1.Namespace{}, errors.New("No namespace") - }) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - - testController.reconcileInstance(instance) - - // verify no kube resources created. - // One single action comes from getting namespace uid - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 1) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - assertUpdateStatus(t, actions[0], instance) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorFindingNamespaceInstanceReason + " " + "Failed to get namespace \"test-ns\" during instance create: No namespace" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileInstanceDelete(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.InstanceClient.Instances = map[string]*brokerapi.ServiceInstance{ - instanceGUID: {}, - } - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - instance := getTestInstance() - instance.ObjectMeta.DeletionTimestamp = &metav1.Time{} - instance.ObjectMeta.Finalizers = []string{"kubernetes"} - - fakeCatalogClient.AddReactor("get", "instances", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, instance, nil - }) - - testController.reconcileInstance(instance) - - // Verify no core kube actions occurred - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - // The three actions should be: - // 0. Updating the ready condition - // 1. Get against the instance - // 2. Removing the finalizer - assertNumberOfActions(t, actions, 3) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyFalse(t, updatedInstance) - - assertGet(t, actions[1], instance) - updatedInstance = assertUpdateStatus(t, actions[2], instance) - assertEmptyFinalizers(t, updatedInstance) - - if _, ok := fakeBrokerClient.InstanceClient.Instances[instanceGUID]; ok { - t.Fatalf("Found the deleted Instance in fakeInstanceClient after deletion") - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeNormal + " " + successDeprovisionReason + " " + "The instance was deprovisioned successfully" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestPollServiceInstanceInProgressProvisioningWithOperation(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "in progress"} - - instance := getTestInstanceAsyncProvisioning(testOperation) - - err := testController.pollInstanceInternal(instance) - if err == nil { - t.Fatalf("Expected pollInstanceInternal to fail while in progress") - } - // Make sure we get an error which means it will get requeued. - if !strings.Contains(err.Error(), "still in progress") { - t.Fatalf("pollInstanceInternal failed but not with expected error, expected %q got %q", "still in progress", err) - } - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 0) - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) -} - -func TestPollServiceInstanceSuccessProvisioningWithOperation(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} - - instance := getTestInstanceAsyncProvisioning(testOperation) - - err := testController.pollInstanceInternal(instance) - if err != nil { - t.Fatalf("pollInstanceInternal failed: %s", err) - } - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - // Instance should be ready and there no longer is an async operation - // in place. - assertInstanceReadyTrue(t, updatedInstance) - assertAsyncOpInProgressFalse(t, updatedInstance) -} - -func TestPollServiceInstanceFailureProvisioningWithOperation(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "failed"} - - instance := getTestInstanceAsyncProvisioning(testOperation) - - err := testController.pollInstanceInternal(instance) - if err != nil { - t.Fatalf("pollInstanceInternal failed: %s", err) - } - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - // Instance should be not ready and there no longer is an async operation - // in place. - assertInstanceReadyFalse(t, updatedInstance) - assertAsyncOpInProgressFalse(t, updatedInstance) -} - -func TestPollServiceInstanceInProgressDeprovisioningWithOperationNoFinalizer(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "in progress"} - - instance := getTestInstanceAsyncDeprovisioning(testOperation) - - err := testController.pollInstanceInternal(instance) - if err == nil { - t.Fatalf("Expected pollInstanceInternal to fail while in progress") - } - // Make sure we get an error which means it will get requeued. - if !strings.Contains(err.Error(), "still in progress") { - t.Fatalf("pollInstanceInternal failed but not with expected error, expected %q got %q", "still in progress", err) - } - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 0) - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) -} - -func TestPollServiceInstanceSuccessDeprovisioningWithOperationNoFinalizer(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} - - instance := getTestInstanceAsyncDeprovisioning(testOperation) - - err := testController.pollInstanceInternal(instance) - if err != nil { - t.Fatalf("pollInstanceInternal failed: %s", err) - } - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - // Instance should have been deprovisioned - assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionFalse, successDeprovisionReason) - assertAsyncOpInProgressFalse(t, updatedInstance) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) -} - -func TestPollServiceInstanceFailureDeprovisioningWithOperation(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "failed"} - - instance := getTestInstanceAsyncDeprovisioning(testOperation) - - err := testController.pollInstanceInternal(instance) - if err != nil { - t.Fatalf("pollInstanceInternal failed: %s", err) - } - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - // Instance should be set to unknown since the operation on the broker - // failed. - assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionUnknown, errorDeprovisionCalledReason) - assertAsyncOpInProgressFalse(t, updatedInstance) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) -} - -func TestPollServiceInstanceStatusGoneDeprovisioningWithOperationNoFinalizer(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusGone - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} - - instance := getTestInstanceAsyncDeprovisioning(testOperation) - - err := testController.pollInstanceInternal(instance) - if err != nil { - t.Fatalf("pollInstanceInternal failed: %s", err) - } - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - // Instance should have been deprovisioned - assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionFalse, successDeprovisionReason) - assertAsyncOpInProgressFalse(t, updatedInstance) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) -} - -func TestPollServiceInstanceSuccessDeprovisioningWithOperationWithFinalizer(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - - // Specify we want asynchronous provisioning... - fakeBrokerClient.InstanceClient.ResponseCode = http.StatusOK - fakeBrokerClient.InstanceClient.LastOperationResponse = &brokerapi.LastOperationResponse{State: "succeeded"} - - instance := getTestInstanceAsyncDeprovisioningWithFinalizer(testOperation) - // updateInstanceFinalizers fetches the latest object. - fakeCatalogClient.AddReactor("get", "instances", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, instance, nil - }) - - err := testController.pollInstanceInternal(instance) - if err != nil { - t.Fatalf("pollInstanceInternal failed: %s", err) - } - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - // The three actions should be: - // 0. Updating the ready condition - // 1. Get against the instance (updateFinalizers calls) - // 2. Removing the finalizer - assertNumberOfActions(t, actions, 3) - - updatedInstance := assertUpdateStatus(t, actions[0], instance) - assertInstanceReadyCondition(t, updatedInstance, v1alpha1.ConditionFalse, successDeprovisionReason) - - // Instance should have been deprovisioned - assertGet(t, actions[1], instance) - updatedInstance = assertUpdateStatus(t, actions[2], instance) - assertEmptyFinalizers(t, updatedInstance) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) -} - -func TestUpdateInstanceCondition(t *testing.T) { - getTestInstanceWithStatus := func(status v1alpha1.ConditionStatus) *v1alpha1.Instance { - instance := getTestInstance() - instance.Status = v1alpha1.InstanceStatus{ - Conditions: []v1alpha1.InstanceCondition{{ - Type: v1alpha1.InstanceConditionReady, - Status: status, - Message: "message", - LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), - }}, - } - - return instance - } - - cases := []struct { - name string - input *v1alpha1.Instance - status v1alpha1.ConditionStatus - reason string - message string - transitionTimeChanged bool - }{ - - { - name: "initially unset", - input: getTestInstance(), - status: v1alpha1.ConditionFalse, - message: "message", - transitionTimeChanged: true, - }, - { - name: "not ready -> not ready", - input: getTestInstanceWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionFalse, - transitionTimeChanged: false, - }, - { - name: "not ready -> not ready, reason and message change", - input: getTestInstanceWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionFalse, - reason: "foo", - message: "bar", - transitionTimeChanged: false, - }, - { - name: "not ready -> ready", - input: getTestInstanceWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionTrue, - message: "message", - transitionTimeChanged: true, - }, - { - name: "ready -> ready", - input: getTestInstanceWithStatus(v1alpha1.ConditionTrue), - status: v1alpha1.ConditionTrue, - message: "message", - transitionTimeChanged: false, - }, - { - name: "ready -> not ready", - input: getTestInstanceWithStatus(v1alpha1.ConditionTrue), - status: v1alpha1.ConditionFalse, - message: "message", - transitionTimeChanged: true, - }, - { - name: "message -> message2", - input: getTestInstanceWithStatus(v1alpha1.ConditionTrue), - status: v1alpha1.ConditionFalse, - message: "message2", - transitionTimeChanged: true, - }, - } - - for _, tc := range cases { - _, fakeCatalogClient, _, testController, _ := newTestController(t) - - clone, err := api.Scheme.DeepCopy(tc.input) - if err != nil { - t.Errorf("%v: deep copy failed", tc.name) - continue - } - inputClone := clone.(*v1alpha1.Instance) - - err = testController.updateInstanceCondition(tc.input, v1alpha1.InstanceConditionReady, tc.status, tc.reason, tc.message) - if err != nil { - t.Errorf("%v: error updating instance condition: %v", tc.name, err) - continue - } - - if !reflect.DeepEqual(tc.input, inputClone) { - t.Errorf("%v: updating broker condition mutated input: expected %v, got %v", tc.name, inputClone, tc.input) - continue - } - - actions := fakeCatalogClient.Actions() - if ok := expectNumberOfActions(t, tc.name, actions, 1); !ok { - continue - } - - updatedInstance, ok := expectUpdateStatus(t, tc.name, actions[0], tc.input) - if !ok { - continue - } - - updateActionObject, ok := updatedInstance.(*v1alpha1.Instance) - if !ok { - t.Errorf("%v: couldn't convert to instance", tc.name) - continue - } - - var initialTs metav1.Time - if len(inputClone.Status.Conditions) != 0 { - initialTs = inputClone.Status.Conditions[0].LastTransitionTime - } - - if e, a := 1, len(updateActionObject.Status.Conditions); e != a { - t.Errorf("%v: expected %v condition(s), got %v", tc.name, e, a) - } - - outputCondition := updateActionObject.Status.Conditions[0] - newTs := outputCondition.LastTransitionTime - - if tc.transitionTimeChanged && initialTs == newTs { - t.Errorf("%v: transition time didn't change when it should have", tc.name) - continue - } else if !tc.transitionTimeChanged && initialTs != newTs { - t.Errorf("%v: transition time changed when it shouldn't have", tc.name) - continue - } - if e, a := tc.reason, outputCondition.Reason; e != "" && e != a { - t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) - continue - } - if e, a := tc.message, outputCondition.Message; e != "" && e != a { - t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) - } - } -} - -func TestReconcileBindingNonExistingInstance(t *testing.T) { - _, fakeCatalogClient, _, testController, _ := newTestController(t) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: "nothere"}, - ExternalID: bindingGUID, - }, - } - - testController.reconcileBinding(binding) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says it failed because no such instance exists. - updateAction := actions[0].(clientgotesting.UpdateAction) - if e, a := "update", updateAction.GetVerb(); e != a { - t.Fatalf("Unexpected verb on actions[0]; expected %v, got %v", e, a) - } - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding, errorNonexistentInstanceReason) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorNonexistentInstanceReason + " " + "Binding \"/test-binding\" references a non-existent Instance \"/nothere\"" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBindingNonExistingServiceClass(t *testing.T) { - _, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - instance := &v1alpha1.Instance{ - ObjectMeta: metav1.ObjectMeta{Name: testInstanceName, Namespace: testNamespace}, - Spec: v1alpha1.InstanceSpec{ - ServiceClassName: "nothere", - PlanName: testPlanName, - ExternalID: instanceGUID, - }, - } - sharedInformers.Instances().Informer().GetStore().Add(instance) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - }, - } - - testController.reconcileBinding(binding) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says it failed because no such service class. - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding, errorNonexistentServiceClassMessage) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorNonexistentServiceClassMessage + " " + "Binding \"test-ns/test-binding\" references a non-existent ServiceClass \"nothere\"" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBindingWithParameters(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - testNsUID := "test_ns_uid" - - fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - UID: types.UID(testNsUID), - }, - }, nil - }) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(getTestInstanceWithStatus(v1alpha1.ConditionTrue)) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - }, - } - - parameters := bindingParameters{Name: "test-param"} - parameters.Args = append(parameters.Args, "first-arg") - parameters.Args = append(parameters.Args, "second-arg") - b, err := json.Marshal(parameters) - if err != nil { - t.Fatalf("Failed to marshal parameters %v : %v", parameters, err) - } - binding.Spec.Parameters = &runtime.RawExtension{Raw: b} - - testController.reconcileBinding(binding) - - if testNsUID != fakeBrokerClient.Bindings[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)].AppID { - t.Fatalf("Unexpected broker AppID: expected %q, got %q", testNsUID, fakeBrokerClient.Bindings[instanceGUID+":"+bindingGUID].AppID) - } - - bindResource := fakeBrokerClient.BindingRequests[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)].BindResource - if appGUID := bindResource["app_guid"]; testNsUID != fmt.Sprintf("%v", appGUID) { - t.Fatalf("Unexpected broker AppID: expected %q, got %q", testNsUID, appGUID) - } - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says binding was created - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyTrue(t, updatedBinding) - - updateObject, ok := updatedBinding.(*v1alpha1.Binding) - if !ok { - t.Fatalf("couldn't convert to *v1alpha1.Binding") - } - - // Verify parameters are what we'd expect them to be, basically name, array with two values in it. - if len(updateObject.Spec.Parameters.Raw) == 0 { - t.Fatalf("Parameters was unexpectedly empty") - } - if b, ok := fakeBrokerClient.BindingClient.Bindings[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)]; !ok { - t.Fatalf("Did not find the created Binding in fakeInstanceBinding after creation") - } else { - if len(b.Parameters) == 0 { - t.Fatalf("Expected parameters, but got none") - } - if e, a := "test-param", b.Parameters["name"].(string); e != a { - t.Fatalf("Unexpected name for parameters: expected %v, got %v", e, a) - } - argsArray := b.Parameters["args"].([]interface{}) - if len(argsArray) != 2 { - t.Fatalf("Expected 2 elements in args array, but got %d", len(argsArray)) - } - foundFirst := false - foundSecond := false - for _, el := range argsArray { - if el.(string) == "first-arg" { - foundFirst = true - } - if el.(string) == "second-arg" { - foundSecond = true - } - } - if !foundFirst { - t.Fatalf("Failed to find 'first-arg' in array, was %v", argsArray) - } - if !foundSecond { - t.Fatalf("Failed to find 'second-arg' in array, was %v", argsArray) - } - } - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeNormal + " " + successInjectedBindResultReason + " " + successInjectedBindResultMessage - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBindingNonbindableServiceClass(t *testing.T) { - _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestNonbindableServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(getTestNonbindableInstance()) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - }, - } - - testController.reconcileBinding(binding) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says binding was created - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding, errorNonbindableServiceClassReason) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorNonbindableServiceClassReason + ` Binding "test-ns/test-binding" references a non-bindable ServiceClass ("test-unbindable-serviceclass") and Plan ("test-unbindable-plan") combination` - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBindingNonbindableServiceClassBindablePlan(t *testing.T) { - _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestNonbindableServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(func() *v1alpha1.Instance { - i := getTestInstanceNonbindableServiceBindablePlan() - i.Status = v1alpha1.InstanceStatus{ - Conditions: []v1alpha1.InstanceCondition{ - { - Type: v1alpha1.InstanceConditionReady, - Status: v1alpha1.ConditionTrue, - }, - }, - } - return i - }()) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - }, - } - - testController.reconcileBinding(binding) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says binding was created - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyTrue(t, updatedBinding) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) -} - -func TestReconcileBindingBindableServiceClassNonbindablePlan(t *testing.T) { - _, fakeCatalogClient, _, testController, sharedInformers := newTestController(t) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(getTestInstanceBindableServiceNonbindablePlan()) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - }, - } - - testController.reconcileBinding(binding) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says binding was created - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding, errorNonbindableServiceClassReason) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorNonbindableServiceClassReason + ` Binding "test-ns/test-binding" references a non-bindable ServiceClass ("test-serviceclass") and Plan ("test-unbindable-plan") combination` - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } -} - -func TestReconcileBindingFailsWithInstanceAsyncOngoing(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(getTestInstanceAsyncProvisioning("")) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - }, - } - - err := testController.reconcileBinding(binding) - if err == nil { - t.Fatalf("reconcileBinding did not fail with async operation ongoing") - } - - if !strings.Contains(err.Error(), "Ongoing Asynchronous") { - t.Fatalf("Did not get the expected error %q : got %q", "Ongoing Asynchronous", err) - } - - // verify no kube resources created. - // No actions - kubeActions := fakeKubeClient.Actions() - assertNumberOfActions(t, kubeActions, 0) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says binding was created - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - if !strings.Contains(events[0], "has ongoing asynchronous operation") { - t.Fatalf("Did not find expected error %q : got %q", "has ongoing asynchronous operation", events[0]) - } - if !strings.Contains(events[0], testNamespace+"/"+testInstanceName) { - t.Fatalf("Did not find expected instance name : got %q", events[0]) - } - if !strings.Contains(events[0], testNamespace+"/"+testBindingName) { - t.Fatalf("Did not find expected binding name : got %q", events[0]) + }, + }, } } -func TestReconcileBindingInstanceNotReady(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - UID: types.UID("test_ns_uid"), +// broker catalog that provides the service class named in of +// getTestServiceClass() +func getTestCatalog() *brokerapi.Catalog { + return &brokerapi.Catalog{ + Services: []*brokerapi.Service{ + { + Name: testServiceClassName, + ID: serviceClassGUID, + Description: "a test service", + Bindable: true, + Plans: []brokerapi.ServicePlan{ + { + Name: testPlanName, + Free: true, + ID: planGUID, + Description: "a test plan", + }, + }, }, - }, nil - }) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(getTestInstance()) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, }, } - - testController.reconcileBinding(binding) - - if _, ok := fakeBrokerClient.Bindings[fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID)]; ok { - t.Fatalf("Unexpected broker binding call") - } - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - - // There should only be one action that says binding was created - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeWarning + " " + errorInstanceNotReadyReason + " " + `Binding cannot begin because referenced instance "test-ns/test-instance" is not ready` - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } } -func TestReconcileBindingNamespaceError(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) - - fakeBrokerClient.CatalogClient.RetCatalog = getTestCatalog() - - fakeKubeClient.AddReactor("get", "namespaces", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, &v1.Namespace{}, errors.New("No namespace") - }) - - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(getTestInstance()) - - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, +// instance referencing the result of getTestServiceClass() +func getTestInstance() *v1alpha1.Instance { + return &v1alpha1.Instance{ + ObjectMeta: metav1.ObjectMeta{Name: testInstanceName, Namespace: testNamespace}, + Spec: v1alpha1.InstanceSpec{ + ServiceClassName: testServiceClassName, + PlanName: testPlanName, + ExternalID: instanceGUID, }, } +} - testController.reconcileBinding(binding) - - actions := fakeCatalogClient.Actions() - assertNumberOfActions(t, actions, 1) - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding) - - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) +// an instance referencing the result of getTestNonbindableServiceClass, on the non-bindable plan. +func getTestNonbindableInstance() *v1alpha1.Instance { + i := getTestInstance() + i.Spec.ServiceClassName = testNonbindableServiceClassName + i.Spec.PlanName = testNonbindablePlanName - expectedEvent := api.EventTypeWarning + " " + errorFindingNamespaceInstanceReason + " " + "Failed to get namespace \"test-ns\" during binding: No namespace" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } + return i } -func TestReconcileBindingDelete(t *testing.T) { - fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController, sharedInformers := newTestController(t) +// an instance referencing the result of getTestNonbindableServiceClass, on the bindable plan. +func getTestInstanceNonbindableServiceBindablePlan() *v1alpha1.Instance { + i := getTestNonbindableInstance() + i.Spec.PlanName = testPlanName - bindingsMapKey := fakebrokerapi.BindingsMapKey(instanceGUID, bindingGUID) + return i +} - fakeBrokerClient.BindingClient.Bindings = map[string]*brokerapi.ServiceBinding{bindingsMapKey: {}} +func getTestInstanceBindableServiceNonbindablePlan() *v1alpha1.Instance { + i := getTestInstance() + i.Spec.PlanName = testNonbindablePlanName - sharedInformers.Brokers().Informer().GetStore().Add(getTestBroker()) - sharedInformers.ServiceClasses().Informer().GetStore().Add(getTestServiceClass()) - sharedInformers.Instances().Informer().GetStore().Add(getTestInstance()) + return i +} - binding := &v1alpha1.Binding{ - ObjectMeta: metav1.ObjectMeta{ - Name: testBindingName, - Namespace: testNamespace, - DeletionTimestamp: &metav1.Time{}, - Finalizers: []string{"kubernetes"}, - }, - Spec: v1alpha1.BindingSpec{ - InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, - ExternalID: bindingGUID, - SecretName: testBindingSecretName, - }, +func getTestInstanceWithStatus(status v1alpha1.ConditionStatus) *v1alpha1.Instance { + instance := getTestInstance() + instance.Status = v1alpha1.InstanceStatus{ + Conditions: []v1alpha1.InstanceCondition{{ + Type: v1alpha1.InstanceConditionReady, + Status: status, + LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), + }}, } - fakeCatalogClient.AddReactor("get", "bindings", func(action clientgotesting.Action) (bool, runtime.Object, error) { - return true, binding, nil - }) - - testController.reconcileBinding(binding) - - kubeActions := fakeKubeClient.Actions() - // The two actions should be: - // 0. Getting the secret - // 1. Deleting the secret - assertNumberOfActions(t, kubeActions, 2) + return instance +} - getAction := kubeActions[0].(clientgotesting.GetActionImpl) - if e, a := "get", getAction.GetVerb(); e != a { - t.Fatalf("Unexpected verb on kubeActions[0]; expected %v, got %v", e, a) +// getTestInstanceAsync returns an instance in async mode +func getTestInstanceAsyncProvisioning(operation string) *v1alpha1.Instance { + instance := getTestInstance() + if operation != "" { + instance.Status.LastOperation = &operation } - - if e, a := binding.Spec.SecretName, getAction.Name; e != a { - t.Fatalf("Unexpected name of secret: expected %v, got %v", e, a) + instance.Status = v1alpha1.InstanceStatus{ + Conditions: []v1alpha1.InstanceCondition{{ + Type: v1alpha1.InstanceConditionReady, + Status: v1alpha1.ConditionFalse, + Message: "Provisioning", + LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), + }}, + AsyncOpInProgress: true, } - deleteAction := kubeActions[1].(clientgotesting.DeleteActionImpl) - if e, a := "delete", deleteAction.GetVerb(); e != a { - t.Fatalf("Unexpected verb on kubeActions[1]; expected %v, got %v", e, a) - } + return instance +} - if e, a := binding.Spec.SecretName, deleteAction.Name; e != a { - t.Fatalf("Unexpected name of secret: expected %v, got %v", e, a) +func getTestInstanceAsyncDeprovisioning(operation string) *v1alpha1.Instance { + instance := getTestInstance() + if operation != "" { + instance.Status.LastOperation = &operation } - - actions := fakeCatalogClient.Actions() - // The three actions should be: - // 0. Updating the ready condition - // 1. Get against the binding in question - // 2. Removing the finalizer - assertNumberOfActions(t, actions, 3) - - updatedBinding := assertUpdateStatus(t, actions[0], binding) - assertBindingReadyFalse(t, updatedBinding) - - assertGet(t, actions[1], binding) - - updatedBinding = assertUpdateStatus(t, actions[2], binding) - assertEmptyFinalizers(t, updatedBinding) - - if _, ok := fakeBrokerClient.BindingClient.Bindings[bindingsMapKey]; ok { - t.Fatalf("Found the deleted Binding in fakeBindingClient after deletion") + instance.Status = v1alpha1.InstanceStatus{ + Conditions: []v1alpha1.InstanceCondition{{ + Type: v1alpha1.InstanceConditionReady, + Status: v1alpha1.ConditionFalse, + Message: "Deprovisioning", + LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), + }}, + AsyncOpInProgress: true, } - events := getRecordedEvents(testController) - assertNumEvents(t, events, 1) - - expectedEvent := api.EventTypeNormal + " " + successUnboundReason + " " + "This binding was deleted successfully" - if e, a := expectedEvent, events[0]; e != a { - t.Fatalf("Received unexpected event: %v", a) - } + // Set the deleted timestamp to simulate deletion + ts := metav1.NewTime(time.Now().Add(-5 * time.Minute)) + instance.DeletionTimestamp = &ts + return instance } -func TestUpdateBindingCondition(t *testing.T) { - getTestBindingWithStatus := func(status v1alpha1.ConditionStatus) *v1alpha1.Binding { - instance := getTestBinding() - instance.Status = v1alpha1.BindingStatus{ - Conditions: []v1alpha1.BindingCondition{{ - Type: v1alpha1.BindingConditionReady, - Status: status, - Message: "message", - LastTransitionTime: metav1.NewTime(time.Now().Add(-5 * time.Minute)), - }}, - } - - return instance - } - - cases := []struct { - name string - input *v1alpha1.Binding - status v1alpha1.ConditionStatus - reason string - message string - transitionTimeChanged bool - }{ +func getTestInstanceAsyncDeprovisioningWithFinalizer(operation string) *v1alpha1.Instance { + instance := getTestInstanceAsyncDeprovisioning(operation) + instance.ObjectMeta.Finalizers = []string{v1alpha1.FinalizerServiceCatalog} + return instance +} - { - name: "initially unset", - input: getTestBinding(), - status: v1alpha1.ConditionFalse, - transitionTimeChanged: true, - }, - { - name: "not ready -> not ready", - input: getTestBindingWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionFalse, - transitionTimeChanged: false, - }, - { - name: "not ready -> not ready, message and reason change", - input: getTestBindingWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionFalse, - reason: "foo", - message: "bar", - transitionTimeChanged: false, - }, - { - name: "not ready -> ready", - input: getTestBindingWithStatus(v1alpha1.ConditionFalse), - status: v1alpha1.ConditionTrue, - transitionTimeChanged: true, - }, - { - name: "ready -> ready", - input: getTestBindingWithStatus(v1alpha1.ConditionTrue), - status: v1alpha1.ConditionTrue, - transitionTimeChanged: false, - }, - { - name: "ready -> not ready", - input: getTestBindingWithStatus(v1alpha1.ConditionTrue), - status: v1alpha1.ConditionFalse, - transitionTimeChanged: true, +// binding referencing the result of getTestInstance() +func getTestBinding() *v1alpha1.Binding { + return &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{Name: testBindingName, Namespace: testNamespace}, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{Name: testInstanceName}, + ExternalID: bindingGUID, }, } +} - for _, tc := range cases { - _, fakeCatalogClient, _, testController, _ := newTestController(t) - - clone, err := api.Scheme.DeepCopy(tc.input) - if err != nil { - t.Errorf("%v: deep copy failed", tc.name) - continue - } - inputClone := clone.(*v1alpha1.Binding) - - err = testController.updateBindingCondition(tc.input, v1alpha1.BindingConditionReady, tc.status, tc.reason, tc.message) - if err != nil { - t.Errorf("%v: error updating broker condition: %v", tc.name, err) - continue - } - - if !reflect.DeepEqual(tc.input, inputClone) { - t.Errorf("%v: updating broker condition mutated input: expected %v, got %v", tc.name, inputClone, tc.input) - continue - } - - actions := fakeCatalogClient.Actions() - if ok := expectNumberOfActions(t, tc.name, actions, 1); !ok { - continue - } - - updatedBinding, ok := expectUpdateStatus(t, tc.name, actions[0], tc.input) - if !ok { - continue - } - - updateActionObject, ok := updatedBinding.(*v1alpha1.Binding) - if !ok { - t.Errorf("%v: couldn't convert to binding", tc.name) - continue - } - - var initialTs metav1.Time - if len(inputClone.Status.Conditions) != 0 { - initialTs = inputClone.Status.Conditions[0].LastTransitionTime - } - - if e, a := 1, len(updateActionObject.Status.Conditions); e != a { - t.Errorf("%v: expected %v condition(s), got %v", tc.name, e, a) - } - - outputCondition := updateActionObject.Status.Conditions[0] - newTs := outputCondition.LastTransitionTime +type instanceParameters struct { + Name string `json:"name"` + Args map[string]string `json:"args"` +} - if tc.transitionTimeChanged && initialTs == newTs { - t.Errorf("%v: transition time didn't change when it should have", tc.name) - continue - } else if !tc.transitionTimeChanged && initialTs != newTs { - t.Errorf("%v: transition time changed when it shouldn't have", tc.name) - continue - } - if e, a := tc.reason, outputCondition.Reason; e != "" && e != a { - t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) - continue - } - if e, a := tc.message, outputCondition.Message; e != "" && e != a { - t.Errorf("%v: condition reasons didn't match; expected %v, got %v", tc.name, e, a) - } - } +type bindingParameters struct { + Name string `json:"name"` + Args []string `json:"args"` } func TestEmptyCatalogConversion(t *testing.T) { @@ -2548,6 +560,62 @@ func TestCatalogConversion(t *testing.T) { checkPlan(serviceClass, 1, "fake-plan-2", "Shared fake Server, 5tb persistent disk, 40 max concurrent connections. 100 async", t) } +func TestCatalogConversionWithAlphaParameterSchemas(t *testing.T) { + catalog := &brokerapi.Catalog{} + err := json.Unmarshal([]byte(alphaParameterSchemaCatalogBytes), &catalog) + if err != nil { + t.Fatalf("Failed to unmarshal test catalog: %v", err) + } + serviceClasses, err := convertCatalog(catalog) + if err != nil { + t.Fatalf("Failed to convertCatalog: %v", err) + } + if len(serviceClasses) != 1 { + t.Fatalf("Expected 1 serviceclasses for testCatalog, but got: %d", len(serviceClasses)) + } + serviceClass := serviceClasses[0] + if len(serviceClass.Plans) != 1 { + t.Fatalf("Expected 1 plan for testCatalog, but got: %d", len(serviceClass.Plans)) + } + + plan := serviceClass.Plans[0] + if plan.AlphaInstanceCreateParameterSchema == nil { + t.Fatalf("Expected plan.AlphaInstanceCreateParameterSchema to be set, but was nil") + } + + cSchema := make(map[string]interface{}) + if err := json.Unmarshal(plan.AlphaInstanceCreateParameterSchema.Raw, &cSchema); err == nil { + schema := make(map[string]interface{}) + if err := json.Unmarshal([]byte(instanceParameterSchemaBytes), &schema); err != nil { + t.Fatalf("Error unmarshalling schema bytes: %v", err) + } + + if e, a := schema, cSchema; !reflect.DeepEqual(e, a) { + t.Fatalf("Unexpected value of alphaInstanceCreateParameterSchema; expected %v, got %v", e, a) + } + } + + if plan.AlphaInstanceUpdateParameterSchema == nil { + t.Fatalf("Expected plan.AlphaInstanceUpdateParameterSchema to be set, but was nil") + } + m := make(map[string]string) + if err := json.Unmarshal(plan.AlphaInstanceUpdateParameterSchema.Raw, &m); err == nil { + if e, a := "zap", m["baz"]; e != a { + t.Fatalf("Unexpected value of alphaInstanceUpdateParameterSchema; expected %v, got %v", e, a) + } + } + + if plan.AlphaBindingCreateParameterSchema == nil { + t.Fatalf("Expected plan.AlphaBindingCreateParameterSchema to be set, but was nil") + } + m = make(map[string]string) + if err := json.Unmarshal(plan.AlphaBindingCreateParameterSchema.Raw, &m); err == nil { + if e, a := "blu", m["zoo"]; e != a { + t.Fatalf("Unexpected value of alphaBindingCreateParameterSchema; expected %v, got %v", e, a) + } + } +} + func checkPlan(serviceClass *v1alpha1.ServiceClass, index int, planName, planDescription string, t *testing.T) { plan := serviceClass.Plans[index] if plan.Name != planName { @@ -2911,6 +979,65 @@ func newTestController(t *testing.T) ( return fakeKubeClient, fakeCatalogClient, fakeBrokerClient, testController.(*controller), serviceCatalogSharedInformers } +type testControllerWithBrokerServer struct { + FakeKubeClient *clientgofake.Clientset + FakeCatalogClient *servicecatalogclientset.Clientset + Controller *controller + Informers v1alpha1informers.Interface + BrokerServerHandler *fakebrokerserver.Handler + BrokerServer *httptest.Server +} + +func (t *testControllerWithBrokerServer) Close() { + t.BrokerServer.Close() +} + +func newTestControllerWithBrokerServer( + brokerUsername, + brokerPassword string, +) (*testControllerWithBrokerServer, error) { + // create a fake kube client + fakeKubeClient := &clientgofake.Clientset{} + // create a fake sc client + fakeCatalogClient := &servicecatalogclientset.Clientset{} + + brokerHandler := fakebrokerserver.NewHandler() + brokerServer := fakebrokerserver.Run(brokerHandler, brokerUsername, brokerPassword) + brokerClFunc := fakebrokerserver.NewCreateFunc(brokerServer, brokerUsername, brokerPassword) + + // create informers + informerFactory := servicecataloginformers.NewSharedInformerFactory(fakeCatalogClient, 0) + serviceCatalogSharedInformers := informerFactory.Servicecatalog().V1alpha1() + + fakeRecorder := record.NewFakeRecorder(5) + + // create a test controller + testController, err := NewController( + fakeKubeClient, + fakeCatalogClient.ServicecatalogV1alpha1(), + serviceCatalogSharedInformers.Brokers(), + serviceCatalogSharedInformers.ServiceClasses(), + serviceCatalogSharedInformers.Instances(), + serviceCatalogSharedInformers.Bindings(), + brokerClFunc, + 24*time.Hour, + true, /* enable OSB context profile */ + fakeRecorder, + ) + if err != nil { + return nil, err + } + + return &testControllerWithBrokerServer{ + FakeKubeClient: fakeKubeClient, + FakeCatalogClient: fakeCatalogClient, + Controller: testController.(*controller), + Informers: serviceCatalogSharedInformers, + BrokerServerHandler: brokerHandler, + BrokerServer: brokerServer, + }, nil +} + func getRecordedEvents(testController *controller) []string { source := testController.recorder.(*record.FakeRecorder).Events done := false diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/openapi/openapi_generated.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/openapi/openapi_generated.go index 9f0fa4ee4c18..6c00a4fea4e4 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/openapi/openapi_generated.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/openapi/openapi_generated.go @@ -28,6 +28,31 @@ import ( func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition { return map[string]openapi.OpenAPIDefinition{ + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.AlphaPodPresetTemplate": { + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "AlphaPodPresetTemplate represents how a PodPreset should be created for a Binding.", + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name is the name of the PodPreset to create.", + Type: []string{"string"}, + Format: "", + }, + }, + "selector": { + SchemaProps: spec.SchemaProps{ + Description: "Selector is the LabelSelector of the PodPreset to create.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"), + }, + }, + }, + Required: []string{"name", "selector"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"}, + }, "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.Binding": { Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -189,12 +214,18 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope Format: "", }, }, + "alphaPodPresetTemplate": { + SchemaProps: spec.SchemaProps{ + Description: "Currently, this field is ALPHA: it may change or disappear at any time and its data will not be migrated.\n\nAlphaPodPresetTemplate describes how a PodPreset should be created once the Binding has been made. If supplied, a PodPreset will be created using information in this field once the Binding has been made in the Broker. The PodPreset will use the EnvFrom feature to expose the keys from the Secret (specified by SecretName) that holds the Binding information into Pods.\n\nIn the future, we will provide a higher degree of control over the PodPreset.", + Ref: ref("github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.AlphaPodPresetTemplate"), + }, + }, }, Required: []string{"instanceRef", "secretName", "externalID"}, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/runtime.RawExtension", "k8s.io/client-go/pkg/api/v1.LocalObjectReference"}, + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.AlphaPodPresetTemplate", "k8s.io/apimachinery/pkg/runtime.RawExtension", "k8s.io/client-go/pkg/api/v1.LocalObjectReference"}, }, "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BindingStatus": { Schema: spec.Schema{ @@ -269,6 +300,23 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope Dependencies: []string{ "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BrokerSpec", "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BrokerStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, }, + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BrokerAuthInfo": { + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "BrokerAuthInfo is a union type that contains information on one of the authentication methods the the service catalog and brokers may support, according to the OpenServiceBroker API specification (https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md).\n\nNote that we currently restrict a single broker to have only one of these fields set on it.", + Properties: map[string]spec.Schema{ + "basicAuthSecret": { + SchemaProps: spec.SchemaProps{ + Description: "BasicAuthSecret is a reference to a Secret containing auth information the catalog should use to authenticate to this Broker using basic auth.", + Ref: ref("k8s.io/client-go/pkg/api/v1.ObjectReference"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/client-go/pkg/api/v1.ObjectReference"}, + }, "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BrokerCondition": { Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -370,10 +418,10 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope Format: "", }, }, - "authSecret": { + "authInfo": { SchemaProps: spec.SchemaProps{ - Description: "AuthSecret is a reference to a Secret containing auth information the catalog should use to authenticate to this Broker.", - Ref: ref("k8s.io/client-go/pkg/api/v1.ObjectReference"), + Description: "AuthInfo contains the data that the service catalog should use to authenticate with the Broker.", + Ref: ref("github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BrokerAuthInfo"), }, }, }, @@ -381,7 +429,7 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope }, }, Dependencies: []string{ - "k8s.io/client-go/pkg/api/v1.ObjectReference"}, + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BrokerAuthInfo"}, }, "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1.BrokerStatus": { Schema: spec.Schema{ @@ -831,6 +879,24 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), }, }, + "alphaInstanceCreateParameterSchema": { + SchemaProps: spec.SchemaProps{ + Description: "Currently, this field is ALPHA: it may change or disappear at any time and its data will not be migrated.\n\nAlphaInstanceCreateParameterSchema is the schema for the parameters that may be supplied when provisioning a new Instance on this plan.", + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + "alphaInstanceUpdateParameterSchema": { + SchemaProps: spec.SchemaProps{ + Description: "Currently, this field is ALPHA: it may change or disappear at any time and its data will not be migrated.\n\nAlphaInstanceUpdateParameterSchema is the schema for the parameters that may be updated once an Instance has been provisioned on this plan. This field only has meaning if the ServiceClass is PlanUpdatable.", + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + "alphaBindingCreateParameterSchema": { + SchemaProps: spec.SchemaProps{ + Description: "Currently, this field is ALPHA: it may change or disappear at any time and its data will not be migrated.\n\nAlphaBindingCreateParameterSchema is the schema for the parameters that may be supplied binding to an Instance on this plan.", + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, }, Required: []string{"name", "externalID", "description", "free"}, }, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/storage.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/storage.go index e1eed59b46c4..89eeb4d65ab1 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/storage.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/storage.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + scmeta "github.com/kubernetes-incubator/service-catalog/pkg/api/meta" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" "github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/server" "github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr" @@ -124,7 +125,7 @@ func NewStorage(opts server.Options) (rest.Storage, rest.Storage, error) { KeyFunc: opts.KeyFunc(true), // Retrieve the name field of the resource. ObjectNameFunc: func(obj runtime.Object) (string, error) { - return tpr.GetAccessor().Name(obj) + return scmeta.GetAccessor().Name(obj) }, // Used to match objects based on labels/fields for list. PredicateFunc: Match, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/strategy.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/strategy.go index 46be65f519ce..c2fb2a8cfab0 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/strategy.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/binding/strategy.go @@ -96,9 +96,7 @@ func (bindingRESTStrategy) PrepareForCreate(ctx genericapirequest.Context, obj r binding.Status = sc.BindingStatus{} // Fill in the first entry set to "creating"? binding.Status.Conditions = []sc.BindingCondition{} - - // TODO: Should we use a more specific string here? - binding.Finalizers = []string{"kubernetes"} + binding.Finalizers = []string{sc.FinalizerServiceCatalog} } func (bindingRESTStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/storage.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/storage.go index 925ce53125e0..3f558893fc17 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/storage.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/storage.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + scmeta "github.com/kubernetes-incubator/service-catalog/pkg/api/meta" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" "github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/server" "github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr" @@ -123,7 +124,7 @@ func NewStorage(opts server.Options) (brokers, brokersStatus rest.Storage) { KeyFunc: opts.KeyFunc(false), // Retrieve the name field of the resource. ObjectNameFunc: func(obj runtime.Object) (string, error) { - return tpr.GetAccessor().Name(obj) + return scmeta.GetAccessor().Name(obj) }, // Used to match objects based on labels/fields for list. PredicateFunc: Match, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/strategy.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/strategy.go index 8b5a41241c52..4cd1e9234599 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/strategy.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/broker/strategy.go @@ -96,9 +96,7 @@ func (brokerRESTStrategy) PrepareForCreate(ctx genericapirequest.Context, obj ru broker.Status = sc.BrokerStatus{} // Fill in the first entry set to "creating"? broker.Status.Conditions = []sc.BrokerCondition{} - - // TODO: Should we use a more specific string here? - broker.Finalizers = []string{"kubernetes"} + broker.Finalizers = []string{sc.FinalizerServiceCatalog} } func (brokerRESTStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/storage.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/storage.go index 5829f5e3ac58..c3a59fd78b1b 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/storage.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/storage.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + scmeta "github.com/kubernetes-incubator/service-catalog/pkg/api/meta" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" "github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/server" "github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr" @@ -123,7 +124,7 @@ func NewStorage(opts server.Options) (rest.Storage, rest.Storage) { KeyFunc: opts.KeyFunc(true), // Retrieve the name field of the resource. ObjectNameFunc: func(obj runtime.Object) (string, error) { - return tpr.GetAccessor().Name(obj) + return scmeta.GetAccessor().Name(obj) }, // Used to match objects based on labels/fields for list. PredicateFunc: Match, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/strategy.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/strategy.go index ed0102050898..63be5ad7c6f1 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/strategy.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/instance/strategy.go @@ -39,12 +39,14 @@ func NewScopeStrategy() rest.NamespaceScopedStrategy { // implements interfaces RESTCreateStrategy, RESTUpdateStrategy, RESTDeleteStrategy, // NamespaceScopedStrategy +// The implementation disallows any modifications to the instance.Status fields. type instanceRESTStrategy struct { runtime.ObjectTyper // inherit ObjectKinds method names.NameGenerator // GenerateName method for CreateStrategy } -// implements interface RESTUpdateStrategy +// implements interface RESTUpdateStrategy. This implementation validates updates to +// instance.Status updates only and disallows any modifications to the instance.Spec. type instanceStatusRESTStrategy struct { instanceRESTStrategy } @@ -95,9 +97,7 @@ func (instanceRESTStrategy) PrepareForCreate(ctx genericapirequest.Context, obj instance.Status = sc.InstanceStatus{} // Fill in the first entry set to "creating"? instance.Status.Conditions = []sc.InstanceCondition{} - - // TODO: Should we use a more specific string here? - instance.Finalizers = []string{"kubernetes"} + instance.Finalizers = []string{sc.FinalizerServiceCatalog} } func (instanceRESTStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { @@ -122,7 +122,13 @@ func (instanceRESTStrategy) PrepareForUpdate(ctx genericapirequest.Context, new, glog.Fatal("received a non-instance object to update from") } + // TODO: We currently don't handle any changes to the spec in the + // reconciler. Once we do that, this check needs to be removed and + // proper validation of allowed changes needs to be implemented in + // ValidateUpdate newInstance.Spec = oldInstance.Spec + + // Do not allow any updates to the Status field while updating the Spec newInstance.Status = oldInstance.Status } @@ -148,7 +154,7 @@ func (instanceStatusRESTStrategy) PrepareForUpdate(ctx genericapirequest.Context if !ok { glog.Fatal("received a non-instance object to update from") } - // status changes are not allowed to update spec + // Status changes are not allowed to update spec newInstance.Spec = oldInstance.Spec foundReadyConditionTrue := false diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/rest/storage_servicecatalog.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/rest/storage_servicecatalog.go index ef525ce9d124..d136243f462f 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/rest/storage_servicecatalog.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/rest/storage_servicecatalog.go @@ -133,6 +133,7 @@ func (p StorageProvider) v1alpha1Storage( ResourceName: tpr.ServiceClassKind.String(), Separator: "/", }, + HardDelete: true, }, p.StorageType, ) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/serviceclass/storage.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/serviceclass/storage.go index b96f097c2c04..f7c0fc8fc0db 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/serviceclass/storage.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/serviceclass/storage.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + scmeta "github.com/kubernetes-incubator/service-catalog/pkg/api/meta" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" "github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/server" "github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr" @@ -124,7 +125,7 @@ func NewStorage(opts server.Options) rest.Storage { KeyFunc: opts.KeyFunc(false), // Retrieve the name field of the resource. ObjectNameFunc: func(obj runtime.Object) (string, error) { - return tpr.GetAccessor().Name(obj) + return scmeta.GetAccessor().Name(obj) }, // Used to match objects based on labels/fields for list. PredicateFunc: Match, diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake/headers.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake/headers.go new file mode 100644 index 000000000000..eff18054d1d4 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake/headers.go @@ -0,0 +1,25 @@ +/* +Copyright 2017 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 fake + +import ( + "net/http" +) + +func setContentType(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake/rest_client.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake/rest_client.go index 0be46f8f8b42..097ce5cbb2b8 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake/rest_client.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake/rest_client.go @@ -19,6 +19,7 @@ package fake import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "log" "net/http" @@ -26,6 +27,7 @@ import ( "time" "github.com/gorilla/mux" + scmeta "github.com/kubernetes-incubator/service-catalog/pkg/api/meta" sc "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/testapi" "k8s.io/apimachinery/pkg/api/meta" @@ -104,13 +106,13 @@ type RESTClient struct { } // NewRESTClient returns a new FakeCoreRESTClient -func NewRESTClient() *RESTClient { +func NewRESTClient(newEmptyObj func() runtime.Object) *RESTClient { storage := make(NamespacedStorage) watcher := NewWatcher() coreCl := &fakerestclient.RESTClient{ Client: fakerestclient.CreateHTTPClient(func(request *http.Request) (*http.Response, error) { - r := getRouter(storage, watcher) + r := getRouter(storage, watcher, newEmptyObj) rw := newResponseWriter() r.ServeHTTP(rw, request) return rw.getResponse(), nil @@ -169,7 +171,11 @@ func (rw *responseWriter) getResponse() *http.Response { } } -func getRouter(storage NamespacedStorage, watcher *Watcher) http.Handler { +func getRouter( + storage NamespacedStorage, + watcher *Watcher, + newEmptyObj func() runtime.Object, +) http.Handler { r := mux.NewRouter() r.StrictSlash(true) r.HandleFunc( @@ -178,7 +184,7 @@ func getRouter(storage NamespacedStorage, watcher *Watcher) http.Handler { ).Methods("GET") r.HandleFunc( "/apis/servicecatalog.k8s.io/v1alpha1/namespaces/{namespace}/{type}", - createItem(storage), + createItem(storage, newEmptyObj), ).Methods("POST") r.HandleFunc( "/apis/servicecatalog.k8s.io/v1alpha1/namespaces/{namespace}/{type}/{name}", @@ -186,7 +192,7 @@ func getRouter(storage NamespacedStorage, watcher *Watcher) http.Handler { ).Methods("GET") r.HandleFunc( "/apis/servicecatalog.k8s.io/v1alpha1/namespaces/{namespace}/{type}/{name}", - updateItem(storage), + updateItem(storage, newEmptyObj), ).Methods("PUT") r.HandleFunc( "/apis/servicecatalog.k8s.io/v1alpha1/namespaces/{namespace}/{type}/{name}", @@ -234,11 +240,15 @@ func getItems(storage NamespacedStorage) func(http.ResponseWriter, *http.Request // in memory, so we're going to make a deep copy first. objCopy, err := conversion.NewCloner().DeepCopy(obj) if err != nil { - log.Fatalf("error performing deep copy: %s", err) + errStr := fmt.Sprintf("error performing deep copy: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } item, ok := objCopy.(runtime.Object) if !ok { - log.Fatalf("error performing type assertion: %s", err) + errStr := fmt.Sprintf("error performing type assertion: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } items = append(items, item) } @@ -249,61 +259,82 @@ func getItems(storage NamespacedStorage) func(http.ResponseWriter, *http.Request case "brokers": list = &sc.BrokerList{TypeMeta: newTypeMeta("broker-list")} if err := meta.SetList(list, items); err != nil { - log.Fatalf("Error setting list items (%s)", err) + errStr := fmt.Sprintf("Error setting list items (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } codec, err = testapi.GetCodecForObject(&sc.BrokerList{}) case "serviceclasses": list = &sc.ServiceClassList{TypeMeta: newTypeMeta("service-class-list")} if err := meta.SetList(list, items); err != nil { - log.Fatalf("Error setting list items (%s)", err) + errStr := fmt.Sprintf("Error setting list items (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } codec, err = testapi.GetCodecForObject(&sc.ServiceClassList{}) case "instances": list = &sc.InstanceList{TypeMeta: newTypeMeta("instance-list")} if err := meta.SetList(list, items); err != nil { - log.Fatalf("Error setting list items (%s)", err) + errStr := fmt.Sprintf("Error setting list items (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } codec, err = testapi.GetCodecForObject(&sc.InstanceList{}) case "bindings": list = &sc.BindingList{TypeMeta: newTypeMeta("binding-list")} if err := meta.SetList(list, items); err != nil { - log.Fatalf("Error setting list items (%s)", err) + errStr := fmt.Sprintf("Error setting list items (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } codec, err = testapi.GetCodecForObject(&sc.BindingList{}) default: - log.Fatalf("unrecognized resource type: %s", tipe) + errStr := fmt.Sprintf("unrecognized resource type: %s", tipe) + http.Error(rw, errStr, http.StatusInternalServerError) + return } if err != nil { - log.Fatalf("error getting codec: %s", err) + errStr := fmt.Sprintf("error getting codec: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } listBytes, err := runtime.Encode(codec, list) if err != nil { - log.Fatalf("error encoding list: %s", err) + errStr := fmt.Sprintf("error encoding list: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } rw.Write(listBytes) } } -func createItem(storage NamespacedStorage) func(rw http.ResponseWriter, r *http.Request) { +func createItem(storage NamespacedStorage, newEmptyObj func() runtime.Object) func(rw http.ResponseWriter, r *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { ns := mux.Vars(r)["namespace"] tipe := mux.Vars(r)["type"] - // TODO: Is there some type-agnostic way to get the codec? - codec, err := testapi.GetCodecForObject(&sc.Broker{}) + codec, err := testapi.GetCodecForObject(newEmptyObj()) if err != nil { - log.Fatalf("error getting codec: %s", err) + errStr := fmt.Sprintf("error getting a codec for %#v (%s)", newEmptyObj(), err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { - log.Fatalf("error getting body bytes: %s", err) + errStr := fmt.Sprintf("error getting body bytes: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } item, err := runtime.Decode(codec, bodyBytes) if err != nil { - log.Fatalf("error decoding body bytes: %s", err) + errStr := fmt.Sprintf("error decoding body bytes: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } name, err := accessor.Name(item) if err != nil { - log.Fatalf("couldn't get object name: %s", err) + errStr := fmt.Sprintf("couldn't get object name: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } if storage.Get(ns, tipe, name) != nil { rw.WriteHeader(http.StatusConflict) @@ -311,10 +342,13 @@ func createItem(storage NamespacedStorage) func(rw http.ResponseWriter, r *http. } accessor.SetResourceVersion(item, "1") storage.Set(ns, tipe, name, item) + setContentType(rw) rw.WriteHeader(http.StatusCreated) bytes, err := runtime.Encode(codec, item) if err != nil { - log.Fatalf("error encoding item: %s", err) + errStr := fmt.Sprintf("error encoding item (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } rw.Write(bytes) } @@ -332,17 +366,22 @@ func getItem(storage NamespacedStorage) func(http.ResponseWriter, *http.Request) } codec, err := testapi.GetCodecForObject(item) if err != nil { - log.Fatalf("error getting codec: %s", err) + errStr := fmt.Sprintf("error getting codec (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } bytes, err := runtime.Encode(codec, item) if err != nil { - log.Fatalf("error encoding item: %s", err) + errStr := fmt.Sprintf("error encoding item (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } + setContentType(rw) rw.Write(bytes) } } -func updateItem(storage NamespacedStorage) func(http.ResponseWriter, *http.Request) { +func updateItem(storage NamespacedStorage, newEmptyObj func() runtime.Object) func(http.ResponseWriter, *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { ns := mux.Vars(r)["namespace"] tipe := mux.Vars(r)["type"] @@ -352,26 +391,35 @@ func updateItem(storage NamespacedStorage) func(http.ResponseWriter, *http.Reque rw.WriteHeader(http.StatusNotFound) return } - // TODO: Is there some type-agnostic way to get the codec? - codec, err := testapi.GetCodecForObject(&sc.Broker{}) + codec, err := testapi.GetCodecForObject(newEmptyObj()) if err != nil { - log.Fatalf("error getting codec: %s", err) + errStr := fmt.Sprintf("error getting codec: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { - log.Fatalf("error getting body bytes: %s", err) + errStr := fmt.Sprintf("error getting body bytes: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } item, err := runtime.Decode(codec, bodyBytes) if err != nil { - log.Fatalf("error decoding body bytes: %s", err) + errStr := fmt.Sprintf("error decoding body bytes: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } - origResourceVersionStr, err := accessor.ResourceVersion(origItem) + origResourceVersionStr, err := accessor.ResourceVersion(item) if err != nil { - log.Fatalf("error getting resource version") + errStr := fmt.Sprintf("error getting resource version") + http.Error(rw, errStr, http.StatusInternalServerError) + return } resourceVersionStr, err := accessor.ResourceVersion(item) if err != nil { - log.Fatalf("error getting resource version") + errStr := fmt.Sprintf("error getting resource version") + http.Error(rw, errStr, http.StatusInternalServerError) + return } // As with the actual core apiserver, "0" is a special resource version that // forces an update as if the current / most up-to-date resource version had @@ -383,11 +431,56 @@ func updateItem(storage NamespacedStorage) func(http.ResponseWriter, *http.Reque resourceVersion, err := strconv.Atoi(origResourceVersionStr) resourceVersion++ accessor.SetResourceVersion(item, strconv.Itoa(resourceVersion)) - storage.Set(ns, tipe, name, item) + + // if the deletion timestamp is set and there are 0 finalizers, then we should delete + finalizers, err := scmeta.GetFinalizers(item) + if err != nil { + errStr := fmt.Sprintf("error getting finalizers (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return + } + oldDT, err := scmeta.GetDeletionTimestamp(origItem) + if err != nil && err != scmeta.ErrNoDeletionTimestamp { + errStr := fmt.Sprintf("error getting deletion timestamp on existing obj (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return + } + newDT, err := scmeta.GetDeletionTimestamp(item) + if err != nil && err != scmeta.ErrNoDeletionTimestamp { + errStr := fmt.Sprintf("error getting deletion timestamp on new obj (%s)", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return + } + if newDT != nil && oldDT != nil { + // only measure the deletion timestamp based on seconds (.Unix()) rather than nanos + // (.UnixNano()), because nanoseconds are stored in memory but do not come over + // the wire. Thus, the new deletion timestamp will not be equal to the old + // because the new will have nanos missing + if newDT.Unix() != oldDT.Unix() { + errStr := fmt.Sprintf( + "you cannot update the deletion timestamp (old: %#v, new: %#v)", + oldDT.String(), + newDT.String(), + ) + http.Error(rw, errStr, http.StatusBadRequest) + return + } + } + + if len(finalizers) == 0 && newDT != nil { + // if there are no finalizers and the deletion timestamp is set, delete + storage.Delete(ns, tipe, name) + } else { + // otherwise, just update as normal + storage.Set(ns, tipe, name, item) + } bytes, err := runtime.Encode(codec, item) if err != nil { - log.Fatalf("error encoding item: %s", err) + errStr := fmt.Sprintf("error encoding item: %s", err) + http.Error(rw, errStr, http.StatusInternalServerError) + return } + setContentType(rw) rw.Write(bytes) } } @@ -402,8 +495,31 @@ func deleteItem(storage NamespacedStorage) func(http.ResponseWriter, *http.Reque rw.WriteHeader(http.StatusNotFound) return } - storage.Delete(ns, tipe, name) - rw.WriteHeader(http.StatusAccepted) + finalizers, err := scmeta.GetFinalizers(item) + if err != nil { + http.Error( + rw, + fmt.Sprintf("error getting finalizers (%s)", err), + http.StatusInternalServerError, + ) + return + } + if len(finalizers) == 0 { + // delete if there are no finalizers + storage.Delete(ns, tipe, name) + } else { + // set a deletion timestamp on the item if there are finalizers + if err := scmeta.SetDeletionTimestamp(item, time.Now()); err != nil { + http.Error( + rw, + fmt.Sprintf("error setting deletion timestamp (%s)", err), + http.StatusInternalServerError, + ) + return + } + storage.Set(ns, tipe, name, item) + } + rw.WriteHeader(http.StatusOK) } } @@ -415,6 +531,7 @@ func listNamespaces(storage NamespacedStorage) func(http.ResponseWriter, *http.R ObjectMeta: metav1.ObjectMeta{Name: ns}, }) } + setContentType(rw) if err := json.NewEncoder(rw).Encode(&nsList); err != nil { log.Printf("Error encoding namespace list (%s)", err) rw.WriteHeader(http.StatusInternalServerError) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/delete.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/delete.go new file mode 100644 index 000000000000..439f1314d42f --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/delete.go @@ -0,0 +1,56 @@ +/* +Copyright 2017 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 tpr + +import ( + "fmt" + "net/http" + + "github.com/golang/glog" + "k8s.io/apiserver/pkg/storage" + restclient "k8s.io/client-go/rest" +) + +func delete(cl restclient.Interface, kind Kind, key, ns, name string, expectedCode int) error { + req := cl.Delete().AbsPath( + "apis", + groupName, + tprVersion, + "namespaces", + ns, + kind.URLName(), + name, + ) + res := req.Do() + if res.Error() != nil { + glog.Errorf("executing DELETE for %s/%s (%s)", ns, name, res.Error()) + } + var statusCode int + res.StatusCode(&statusCode) + if statusCode == http.StatusNotFound { + return storage.NewKeyNotFoundError(key, 0) + } + if statusCode != expectedCode { + return fmt.Errorf( + "executing DELETE for %s/%s, received response code %d", + ns, + name, + statusCode, + ) + } + return nil +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/get.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/get.go new file mode 100644 index 000000000000..703cd89cd26d --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/get.go @@ -0,0 +1,88 @@ +/* +Copyright 2017 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 tpr + +import ( + "fmt" + "net/http" + + "github.com/golang/glog" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/storage" + restclient "k8s.io/client-go/rest" +) + +func get( + cl restclient.Interface, + codec runtime.Codec, + kind Kind, + key, + ns, + name string, + out runtime.Object, + hasNamespace, + ignoreNotFound bool, +) error { + req := cl.Get().AbsPath( + "apis", + groupName, + tprVersion, + "namespaces", + ns, + kind.URLName(), + name, + ) + + res := req.Do() + if res.Error() != nil { + glog.Errorf("executing GET for %s/%s (%s)", ns, name, res.Error()) + } + var statusCode int + res.StatusCode(&statusCode) + if statusCode == http.StatusNotFound { + if ignoreNotFound { + return runtime.SetZeroValue(out) + } + glog.Errorf("executing GET for %s/%s: not found", ns, name) + return storage.NewKeyNotFoundError(key, 0) + } + if statusCode != http.StatusOK { + return fmt.Errorf( + "executing GET for %s/%s, received response code %d", + ns, + name, + statusCode, + ) + } + + var unknown runtime.Unknown + if err := res.Into(&unknown); err != nil { + glog.Errorf("decoding response (%s)", err) + return err + } + + if err := decode(codec, unknown.Raw, out); err != nil { + return nil + } + if !hasNamespace { + if err := removeNamespace(out); err != nil { + glog.Errorf("removing namespace from %#v (%s)", out, err) + return err + } + } + return nil +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/list_resources_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/list_resources_test.go index 38c202771248..eeaa186fffc4 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/list_resources_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/list_resources_test.go @@ -24,6 +24,7 @@ import ( "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/testapi" "github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) func TestStripNamespacesFromList(t *testing.T) { @@ -57,7 +58,9 @@ func TestGetAllNamespaces(t *testing.T) { const ( ns1Name = "ns1" ) - cl := fake.NewRESTClient() + cl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) nsList, err := getAllNamespaces(cl) if err != nil { t.Fatalf("getting all namespaces (%s)", err) @@ -84,7 +87,9 @@ func TestListResource(t *testing.T) { kind = ServiceBrokerKind ) - cl := fake.NewRESTClient() + cl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) listObj := sc.BrokerList{TypeMeta: newTypeMeta(kind)} codec, err := testapi.GetCodecForObject(&sc.BrokerList{TypeMeta: newTypeMeta(kind)}) if err != nil { diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/options.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/options.go index 1930c77d9d4e..57fa394da04d 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/options.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/options.go @@ -29,10 +29,15 @@ type Options struct { DefaultNamespace string RESTClient restclient.Interface SingularKind Kind - NewSingularFunc func(string, string) runtime.Object - ListKind Kind - NewListFunc func() runtime.Object - CheckObjectFunc func(runtime.Object) error - DestroyFunc func() - Keyer Keyer + // NewSingularFunc is a function that returns a new object of the appropriate type, + // with the namespace (first param) and name (second param) pre-filled + NewSingularFunc func(string, string) runtime.Object + ListKind Kind + // NewListFunc is a function that returns a new, empty list object of the appropriate + // type. The list object should hold elements that are returned by NewSingularFunc + NewListFunc func() runtime.Object + CheckObjectFunc func(runtime.Object) error + DestroyFunc func() + Keyer Keyer + HardDelete bool } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/put.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/put.go new file mode 100644 index 000000000000..9d5fd50a2af4 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/put.go @@ -0,0 +1,71 @@ +/* +Copyright 2017 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 tpr + +import ( + "fmt" + "net/http" + + "github.com/golang/glog" + "k8s.io/apimachinery/pkg/runtime" + restclient "k8s.io/client-go/rest" +) + +func put( + cl restclient.Interface, + codec runtime.Codec, + kind Kind, + ns, + name string, + data []byte, + out runtime.Object, +) error { + putReq := cl.Put().AbsPath( + "apis", + groupName, + tprVersion, + "namespaces", + ns, + kind.URLName(), + name, + ).Body(data) + putRes := putReq.Do() + if putRes.Error() != nil { + glog.Errorf("executing PUT to %s/%s (%s)", ns, name, putRes.Error()) + return putRes.Error() + } + var statusCode int + putRes.StatusCode(&statusCode) + if statusCode != http.StatusOK { + return fmt.Errorf( + "executing PUT for %s/%s, received response code %d", + ns, + name, + statusCode, + ) + } + var putUnknown runtime.Unknown + if err := putRes.Into(&putUnknown); err != nil { + glog.Errorf("reading response (%s)", err) + return err + } + if err := decode(codec, putUnknown.Raw, out); err != nil { + glog.Errorf("decoding response (%s)", err) + return err + } + return nil +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface.go index c007a6ff3e3b..6536148d6ac4 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface.go @@ -23,6 +23,8 @@ import ( "net/http" "github.com/golang/glog" + scmeta "github.com/kubernetes-incubator/service-catalog/pkg/api/meta" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" "golang.org/x/net/context" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -45,12 +47,17 @@ type store struct { defaultNamespace string cl restclient.Interface singularKind Kind - singularShell func(string, string) runtime.Object - listKind Kind - listShell func() runtime.Object - checkObject func(runtime.Object) error - decodeKey func(string) (string, string, error) - versioner storage.Versioner + // singularShell is a function that returns a new object of the appropriate type, + // with the namespace (first param) and name (second param) pre-filled + singularShell func(string, string) runtime.Object + listKind Kind + // listShell is a function that returns a new, empty list object of the appropriate + // type. The list object should hold elements that are returned by singularShell + listShell func() runtime.Object + checkObject func(runtime.Object) error + decodeKey func(string) (string, string, error) + versioner storage.Versioner + hardDelete bool } // NewStorage creates a new TPR-based storage.Interface implementation @@ -67,6 +74,7 @@ func NewStorage(opts Options) (storage.Interface, factory.DestroyFunc) { checkObject: opts.CheckObjectFunc, decodeKey: opts.Keyer.NamespaceAndNameFromKey, versioner: etcd.APIObjectVersioner{}, + hardDelete: opts.HardDelete, }, opts.DestroyFunc } @@ -92,6 +100,10 @@ func (t *store) Create( return err } + if err := scmeta.AddFinalizer(obj, v1alpha1.FinalizerServiceCatalog); err != nil { + glog.Errorf("adding finalizer to %s (%s)", key, err) + return err + } data, err := runtime.Encode(t.codec, obj) if err != nil { return err @@ -107,7 +119,10 @@ func (t *store) Create( res := req.Do() if res.Error() != nil { - glog.Errorf("executing POST for %s/%s (%s)", ns, name, res.Error()) + errStr := fmt.Sprintf("executing POST for %s/%s (%s)", ns, name, res.Error()) + glog.Errorf(errStr) + // Don't return an error here so that, in case there was a 409 (conflict), we go and + // return the key exists error } var statusCode int res.StatusCode(&statusCode) @@ -115,12 +130,14 @@ func (t *store) Create( return storage.NewKeyExistsError(key, 0) } if statusCode != http.StatusCreated { - return fmt.Errorf( + errStr := fmt.Sprintf( "executing POST for %s/%s, received response code %d", ns, name, statusCode, ) + glog.Errorf(errStr) + return errors.New(errStr) } var unknown runtime.Unknown @@ -135,50 +152,60 @@ func (t *store) Create( return nil } -// Delete removes the specified key and returns the value that existed at that spot. -// If key didn't exist, it will return NotFound storage error. +// Delete fetches the resource at key, removes its finalizer, updates it, and returns the +// resource before its finalizer was removed. // -// In this implementation, Delete will not write the deleted object back to out +// If key didn't exist, it will return NotFound storage error. func (t *store) Delete( ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, ) error { + // create adds the get the object remove its finalizer, and ns, name, err := t.decodeKey(key) if err != nil { glog.Errorf("decoding key %s (%s)", key, err) return err } - - req := t.cl.Delete().AbsPath( - "apis", - groupName, - tprVersion, - "namespaces", + if t.hardDelete { + // if we are hard-deleting this item, then propagate this delete to the core API server. + // after the core API server gets the DELETE call, it will set the deletion timestamp + // as we expect, so we should proceed to remove the deletion timestamp & update as usual + // (below), so that the object is removed completely + if err := delete(t.cl, t.singularKind, key, ns, name, http.StatusOK); err != nil { + glog.Errorf("hard-deleting %s (%s)", key, err) + return err + } + } + if err := get( + t.cl, + t.codec, + t.singularKind, + key, ns, - t.singularKind.URLName(), name, - ) + out, + t.hasNamespace, + false, + ); err != nil { + glog.Errorf("getting %s (%s)", key, err) + return err + } - res := req.Do() - if res.Error() != nil { - glog.Errorf("executing DELETE for %s/%s (%s)", ns, name, res.Error()) + if _, err := scmeta.RemoveFinalizer(out, v1alpha1.FinalizerServiceCatalog); err != nil { + glog.Errorf("removing finalizer from %#v (%s)", out, err) + return err } - var statusCode int - res.StatusCode(&statusCode) - if statusCode == http.StatusNotFound { - return storage.NewKeyNotFoundError(key, 0) + encoded, err := runtime.Encode(t.codec, out) + if err != nil { + glog.Errorf("encoding %#v (%s)", out, err) + return err } - if statusCode != http.StatusAccepted { - return fmt.Errorf( - "executing DELETE for %s/%s, received response code %d", - ns, - name, - statusCode, - ) + if err := put(t.cl, t.codec, t.singularKind, ns, name, encoded, out); err != nil { + glog.Errorf("putting %s (%s)", key, err) + return err } - return nil } @@ -501,78 +528,82 @@ func (t *store) GuaranteedUpdate( glog.Errorf("checking preconditions (%s)", err) return err } - // Create a candidate for the new object by applying the userUpdate func - candidate, _, err := userUpdate(curState.obj, *curState.meta) + // update the object by applying the userUpdate func & encode it + updated, _, err := userUpdate(curState.obj, *curState.meta) if err != nil { glog.Errorf("applying user update: (%s)", err) return err } - // Get bytes from the candidate - candidateData, err := runtime.Encode(t.codec, candidate) + updatedData, err := runtime.Encode(t.codec, updated) if err != nil { glog.Errorf("encoding candidate obj (%s)", err) return err } - // If the candidate matches what we already have, then all we need to do is - // decode into the out object - if bytes.Equal(candidateData, curState.data) { - err := decode(t.codec, candidateData, out) + + // figure out what the new "current state" of the object is for this loop iteration + var newCurState *objState + if bytes.Equal(updatedData, curState.data) { + // If the candidate matches what we already have, then all we need to do is + // decode into the out object + err := decode(t.codec, updatedData, out) if err != nil { glog.Errorf("decoding to output object (%s)", err) } - return err - } - // Otherwise, get an up-to-date copy of the resource we're trying to update - // (because it may have changed if we're looping and in a race) - newCurObj := t.singularShell("", "") - if err := t.Get(ctx, key, "", newCurObj, ignoreNotFound); err != nil { - glog.Errorf("getting new current object (%s)", err) - return err + newCurState = curState + } else { + // If the candidate doesn't match what we already have, then get an up-to-date copy + // of the resource we're trying to update + // (because it may have changed if we're looping and in a race) + newCurObj := t.singularShell("", "") + if err := t.Get(ctx, key, "", newCurObj, ignoreNotFound); err != nil { + glog.Errorf("getting new current object (%s)", err) + return err + } + updatedObj, _, err := userUpdate(newCurObj, *curState.meta) + ncs, err := t.getStateFromObject(updatedObj) + if err != nil { + glog.Errorf("getting state from new current object (%s)", err) + return err + } + newCurState = ncs } - newCurState, err := t.getStateFromObject(newCurObj) + newCurObjData, err := runtime.Encode(t.codec, newCurState.obj) if err != nil { - glog.Errorf("getting state from new current object (%s)", err) + glog.Errorf("encoding new obj (%s)", err) return err } - // If the new current version of the object is the same as the old current - // then proceed with trying to PUT the candidate to the core apiserver + // If the new current revision of the object is the same as the last loop iteration, + // proceed with trying to update the object on the core API server if newCurState.rev == curState.rev { ns, name, err := t.decodeKey(key) if err != nil { glog.Errorf("decoding key %s (%s)", key, err) return err } - putReq := t.cl.Put().AbsPath( - "apis", - groupName, - tprVersion, - "namespaces", - ns, - t.singularKind.URLName(), - name, - ).Body(candidateData) - putRes := putReq.Do() - if putRes.Error() != nil { - glog.Errorf("executing PUT to %s/%s (%s)", ns, name, putRes.Error()) + newStateDTExists, err := getDeletionInfo(newCurState.obj) + if err != nil { + glog.Errorf("getting deletion info (%s)", err) return err } - var statusCode int - putRes.StatusCode(&statusCode) - if statusCode != http.StatusOK { - return fmt.Errorf( - "executing PUT for %s/%s, received response code %d", - ns, - name, - statusCode, - ) - } - var putUnknown runtime.Unknown - if err := putRes.Into(&putUnknown); err != nil { - glog.Errorf("reading response (%s)", err) + finalizers, err := scmeta.GetFinalizers(newCurState.obj) + if err != nil { + glog.Errorf("getting finalizers (%s)", err) return err } - if err := decode(t.codec, putUnknown.Raw, out); err != nil { - glog.Errorf("decoding response (%s)", err) + if newStateDTExists && len(finalizers) > 0 { + // if the deletion timestamp is set but there are still finalizers, then send + // a DELETE to the upstream server. + // The upstream server will do a soft delete and set the deletion timestamp + if err := delete(t.cl, t.singularKind, key, ns, name, http.StatusOK); err != nil { + glog.Errorf("executing DELETE on %s (%s)", key, err) + return err + } + return nil + } + // otherwise, the deletion timestamp and deletion grace period are not set, so + // do the actual update + if err := put(t.cl, t.codec, t.singularKind, ns, name, newCurObjData, out); err != nil { + glog.Errorf("PUTting object %s (%s)", key, err) return err } } else { @@ -603,7 +634,7 @@ func decode( } func removeNamespace(obj runtime.Object) error { - if err := accessor.SetNamespace(obj, ""); err != nil { + if err := scmeta.GetAccessor().SetNamespace(obj, ""); err != nil { glog.Errorf("removing namespace from %#v (%s)", obj, err) return err } @@ -634,3 +665,14 @@ func checkPreconditions( } return nil } + +// getDeletionInfo returns whether the deletion timestsamp exists on obj +// if there was an error determining whether it exists, returns a non-nil error +func getDeletionInfo(obj runtime.Object) (bool, error) { + dtExists, err := scmeta.DeletionTimestampExists(obj) + if err != nil { + glog.Errorf("determining whether the deletion timestamp exists (%s)", err) + return false, err + } + return dtExists, nil +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface_test.go index 00370a1d59ac..5ad1f9512a97 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/pkg/storage/tpr/storage_interface_test.go @@ -23,10 +23,12 @@ import ( "reflect" "testing" + scmeta "github.com/kubernetes-incubator/service-catalog/pkg/api/meta" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" sc "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" _ "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/install" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/testapi" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" "github.com/kubernetes-incubator/service-catalog/pkg/rest/core/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -44,7 +46,9 @@ const ( func TestCreateExistingWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) // Ensure an existing broker fakeCl.Storage.Set(globalNamespace, ServiceBrokerKind.URLName(), name, &sc.Broker{ @@ -81,7 +85,9 @@ func TestCreateExistingWithNoNamespace(t *testing.T) { func TestCreateExistingWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) // Ensure an existing instance fakeCl.Storage.Set(namespace, ServiceInstanceKind.URLName(), name, &sc.Instance{ @@ -126,7 +132,9 @@ func TestCreateExistingWithNamespace(t *testing.T) { func TestCreateWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) inputBroker := &sc.Broker{ ObjectMeta: metav1.ObjectMeta{Name: name}, @@ -174,7 +182,9 @@ func TestCreateWithNoNamespace(t *testing.T) { func TestCreateWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) inputInstance := &sc.Instance{ ObjectMeta: metav1.ObjectMeta{ @@ -228,7 +238,9 @@ func TestCreateWithNamespace(t *testing.T) { func TestGetNonExistentWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) key, err := keyer.Key(request.NewContext(), name) if err != nil { @@ -270,7 +282,9 @@ func TestGetNonExistentWithNoNamespace(t *testing.T) { func TestGetNonExistentWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) ctx := request.NewContext() ctx = request.WithNamespace(ctx, namespace) @@ -314,7 +328,9 @@ func TestGetNonExistentWithNamespace(t *testing.T) { func TestGetWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) // Ensure an existing broker fakeCl.Storage.Set(globalNamespace, ServiceBrokerKind.URLName(), name, &sc.Broker{ @@ -348,7 +364,9 @@ func TestGetWithNoNamespace(t *testing.T) { func TestGetWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) // Ensure an existing instance fakeCl.Storage.Set(namespace, ServiceInstanceKind.URLName(), name, &sc.Instance{ @@ -391,7 +409,9 @@ func TestGetWithNamespace(t *testing.T) { func TestGetEmptyListWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.BrokerList{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) key := keyer.KeyRoot(request.NewContext()) outBrokerList := &sc.BrokerList{} @@ -432,7 +452,9 @@ func TestGetEmptyListWithNoNamespace(t *testing.T) { func TestGetEmptyListWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.InstanceList{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) ctx := request.NewContext() ctx = request.WithNamespace(ctx, namespace) @@ -475,7 +497,9 @@ func TestGetEmptyListWithNamespace(t *testing.T) { func TestGetListWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.BrokerList{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) // Ensure an existing broker fakeCl.Storage.Set(globalNamespace, ServiceBrokerKind.URLName(), name, &sc.Broker{ @@ -516,7 +540,9 @@ func TestGetListWithNoNamespace(t *testing.T) { func TestGetListWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.InstanceList{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) // Ensure an existing instance fakeCl.Storage.Set(globalNamespace, ServiceInstanceKind.URLName(), name, &sc.Instance{ @@ -564,7 +590,9 @@ func TestGetListWithNamespace(t *testing.T) { func TestUpdateNonExistentWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) key, err := keyer.Key(request.NewContext(), name) newURL := "http://your-incredible-broker.io" @@ -615,7 +643,9 @@ func TestUpdateNonExistentWithNoNamespace(t *testing.T) { func TestUpdateNonExistentWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) ctx := request.NewContext() ctx = request.WithNamespace(ctx, namespace) @@ -668,7 +698,9 @@ func TestUpdateNonExistentWithNamespace(t *testing.T) { func TestUpdateWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) var origRev uint64 = 1 newURL := "http://your-incredible-broker.io" @@ -719,7 +751,9 @@ func TestUpdateWithNoNamespace(t *testing.T) { func TestUpdateWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) var origRev uint64 = 1 newPlanName := "my-really-awesome-plan" @@ -772,7 +806,9 @@ func TestUpdateWithNamespace(t *testing.T) { func TestDeleteNonExistentWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) key, err := keyer.Key(request.NewContext(), name) if err != nil { @@ -797,7 +833,9 @@ func TestDeleteNonExistentWithNoNamespace(t *testing.T) { func TestDeleteNonExistentWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) ctx := request.NewContext() ctx = request.WithNamespace(ctx, namespace) @@ -824,15 +862,20 @@ func TestDeleteNonExistentWithNamespace(t *testing.T) { func TestDeleteWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) var origRev uint64 = 1 - fakeCl.Storage.Set(globalNamespace, ServiceBrokerKind.URLName(), name, &sc.Broker{ + brokerNoFinalizers := &sc.Broker{ ObjectMeta: metav1.ObjectMeta{ Name: name, ResourceVersion: fmt.Sprintf("%d", origRev), }, - }) + } + brokerWithFinalizers := *brokerNoFinalizers + brokerWithFinalizers.Finalizers = append(brokerWithFinalizers.Finalizers, v1alpha1.FinalizerServiceCatalog) + fakeCl.Storage.Set(globalNamespace, ServiceBrokerKind.URLName(), name, &brokerWithFinalizers) key, err := keyer.Key(request.NewContext(), name) if err != nil { t.Fatalf("error constructing key (%s)", err) @@ -849,24 +892,40 @@ func TestDeleteWithNoNamespace(t *testing.T) { } // Object should be removed from underlying storage obj := fakeCl.Storage.Get(globalNamespace, ServiceBrokerKind.URLName(), name) - if obj != nil { - t.Fatalf( - "expected object to be removed from underlying sotrage, but it was not", - ) + finalizers, err := scmeta.GetFinalizers(obj) + if err != nil { + t.Fatalf("error getting finalizers (%s)", err) + } + if len(finalizers) != 0 { + t.Fatalf("expected no finalizers, got %#v", finalizers) + } + // the delete call does a PUT, which increments the resource version. brokerNoFinalizers + // and obj should match exactly except for the resource version, so do the increment here + brokerNoFinalizers.ResourceVersion = fmt.Sprintf("%d", origRev+1) + if err := deepCompare("expected", brokerNoFinalizers, "actual", obj); err != nil { + t.Fatal(err) } } func TestDeleteWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) var origRev uint64 = 1 - fakeCl.Storage.Set(namespace, ServiceInstanceKind.URLName(), name, &sc.Instance{ + instanceNoFinalizers := &sc.Instance{ ObjectMeta: metav1.ObjectMeta{ Name: name, ResourceVersion: fmt.Sprintf("%d", origRev), }, - }) + Spec: sc.InstanceSpec{ + ExternalID: "76026cec-f601-487f-b6bd-6d6f8240d620", + }, + } + instanceWithFinalizers := *instanceNoFinalizers + instanceWithFinalizers.Finalizers = append(instanceWithFinalizers.Finalizers, v1alpha1.FinalizerServiceCatalog) + fakeCl.Storage.Set(namespace, ServiceInstanceKind.URLName(), name, &instanceWithFinalizers) ctx := request.NewContext() ctx = request.WithNamespace(ctx, namespace) key, err := keyer.Key(ctx, name) @@ -885,16 +944,26 @@ func TestDeleteWithNamespace(t *testing.T) { } // Object should be removed from underlying storage obj := fakeCl.Storage.Get(namespace, ServiceInstanceKind.URLName(), name) - if obj != nil { - t.Fatalf( - "expected object to be removed from underlying sotrage, but it was not", - ) + finalizers, err := scmeta.GetFinalizers(obj) + if err != nil { + t.Fatalf("error getting finalizers (%s)", err) + } + if len(finalizers) != 0 { + t.Fatalf("expected no finalizers, got %#v", finalizers) + } + // the delete call does a PUT, which increments the resource version. brokerNoFinalizers + // and obj should match exactly except for the resource version, so do the increment here + instanceNoFinalizers.ResourceVersion = fmt.Sprintf("%d", origRev+1) + if err := deepCompare("expected", instanceNoFinalizers, "actual", obj); err != nil { + t.Fatal(err) } } func TestWatchWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Instance{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) obj := &sc.Instance{ TypeMeta: metav1.TypeMeta{Kind: ServiceInstanceKind.String()}, @@ -916,7 +985,9 @@ func TestWatchWithNamespace(t *testing.T) { func TestWatchWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.Broker{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) obj := &sc.Broker{ TypeMeta: metav1.TypeMeta{Kind: ServiceBrokerKind.String()}, @@ -936,7 +1007,9 @@ func TestWatchWithNoNamespace(t *testing.T) { func TestWatchListWithNamespace(t *testing.T) { keyer := getInstanceKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.InstanceList{} + }) iface := getInstanceTPRStorageIFace(t, keyer, fakeCl) obj := &sc.InstanceList{ @@ -970,7 +1043,9 @@ func TestWatchListWithNamespace(t *testing.T) { func TestWatchListWithNoNamespace(t *testing.T) { keyer := getBrokerKeyer() - fakeCl := fake.NewRESTClient() + fakeCl := fake.NewRESTClient(func() runtime.Object { + return &sc.BrokerList{} + }) iface := getBrokerTPRStorageIFace(t, keyer, fakeCl) obj := &sc.BrokerList{ Items: []sc.Broker{ diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/broker.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/broker.go index d135393f7d4d..2df30a0cb13f 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/broker.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/broker.go @@ -21,8 +21,6 @@ import ( "github.com/kubernetes-incubator/service-catalog/test/e2e/framework" "github.com/kubernetes-incubator/service-catalog/test/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/client-go/pkg/api/v1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -39,57 +37,6 @@ func newTestBroker(name, url string) *v1alpha1.Broker { } } -func newTestBrokerPod(name string) *v1.Pod { - return &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Labels: map[string]string{ - "app": name, - }, - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: name, - Image: "quay.io/kubernetes-service-catalog/user-broker:v0.0.7", - Args: []string{ - "--port", - "8080", - }, - Ports: []v1.ContainerPort{ - { - ContainerPort: 8080, - }, - }, - }, - }, - }, - } -} - -func newTestBrokerService(name string) *v1.Service { - return &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Labels: map[string]string{ - "app": name, - }, - }, - Spec: v1.ServiceSpec{ - Selector: map[string]string{ - "app": name, - }, - Ports: []v1.ServicePort{ - { - Protocol: v1.ProtocolTCP, - Port: 80, - TargetPort: intstr.FromInt(8080), - }, - }, - }, - } -} - var _ = framework.ServiceCatalogDescribe("Broker", func() { f := framework.NewDefaultFramework("create-broker") @@ -97,13 +44,13 @@ var _ = framework.ServiceCatalogDescribe("Broker", func() { BeforeEach(func() { By("Creating a user broker pod") - pod, err := f.KubeClientSet.CoreV1().Pods(f.Namespace.Name).Create(newTestBrokerPod(brokerName)) + pod, err := f.KubeClientSet.CoreV1().Pods(f.Namespace.Name).Create(NewUPSBrokerPod(brokerName)) Expect(err).NotTo(HaveOccurred()) By("Waiting for pod to be running") err = framework.WaitForPodRunningInNamespace(f.KubeClientSet, pod) Expect(err).NotTo(HaveOccurred()) By("Creating a user broker service") - _, err = f.KubeClientSet.CoreV1().Services(f.Namespace.Name).Create(newTestBrokerService(brokerName)) + _, err = f.KubeClientSet.CoreV1().Services(f.Namespace.Name).Create(NewUPSBrokerService(brokerName)) Expect(err).NotTo(HaveOccurred()) }) @@ -130,7 +77,13 @@ var _ = framework.ServiceCatalogDescribe("Broker", func() { Status: v1alpha1.ConditionTrue, }) Expect(err).NotTo(HaveOccurred()) + By("Deleting the Broker") - f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Brokers().Delete(brokerName, nil) + err = f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Brokers().Delete(brokerName, nil) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Broker to not exist") + err = util.WaitForBrokerToNotExist(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), brokerName) + Expect(err).NotTo(HaveOccurred()) }) }) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/framework/util.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/framework/util.go index aee7cf2c0c13..ed1819bdf959 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/framework/util.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/framework/util.go @@ -36,7 +36,7 @@ import ( const ( // How often to poll for conditions - defaultPoll = 2 * time.Second + Poll = 2 * time.Second // Default time to wait for operations to complete defaultTimeout = 30 * time.Second @@ -104,7 +104,7 @@ func CreateKubeNamespace(baseName string, c kubernetes.Interface) (*v1.Namespace Logf("namespace: %v", ns) // Be robust about making the namespace creation call. var got *v1.Namespace - err := wait.PollImmediate(defaultPoll, defaultTimeout, func() (bool, error) { + err := wait.PollImmediate(Poll, defaultTimeout, func() (bool, error) { var err error got, err = c.Core().Namespaces().Create(ns) if err != nil { @@ -140,7 +140,7 @@ func WaitForPodRunningInNamespace(c kubernetes.Interface, pod *v1.Pod) error { } func waitTimeoutForPodRunningInNamespace(c kubernetes.Interface, podName, namespace string, timeout time.Duration) error { - return wait.PollImmediate(defaultPoll, defaultTimeout, podRunning(c, podName, namespace)) + return wait.PollImmediate(Poll, defaultTimeout, podRunning(c, podName, namespace)) } func podRunning(c kubernetes.Interface, podName, namespace string) wait.ConditionFunc { diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/instance.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/instance.go new file mode 100644 index 000000000000..7a75e6e50470 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/instance.go @@ -0,0 +1,90 @@ +/* +Copyright 2017 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 e2e + +import ( + "time" + + v1alpha1 "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/clientset" + "github.com/kubernetes-incubator/service-catalog/test/e2e/framework" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const ( + // how long to wait for an instance to be deleted. + instanceDeleteTimeout = 30 * time.Second +) + +func newTestInstance(name, serviceClassName, planName string) *v1alpha1.Instance { + return &v1alpha1.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: v1alpha1.InstanceSpec{ + PlanName: planName, + ServiceClassName: serviceClassName, + }, + } +} + +// createInstance in the specified namespace +func createInstance(c clientset.Interface, namespace string, instance *v1alpha1.Instance) (*v1alpha1.Instance, error) { + return c.ServicecatalogV1alpha1().Instances(namespace).Create(instance) +} + +// deleteInstance with the specified namespace and name +func deleteInstance(c clientset.Interface, namespace, name string) error { + return c.ServicecatalogV1alpha1().Instances(namespace).Delete(name, nil) +} + +// waitForInstanceToBeDeleted waits for the instance to be removed. +func waitForInstanceToBeDeleted(c clientset.Interface, namespace, name string) error { + return wait.Poll(framework.Poll, instanceDeleteTimeout, func() (bool, error) { + _, err := c.ServicecatalogV1alpha1().Instances(namespace).Get(name, metav1.GetOptions{}) + if err == nil { + framework.Logf("waiting for instance %s to be deleted", name) + return false, nil + } + if errors.IsNotFound(err) { + framework.Logf("verified instance %s is deleted", name) + return true, nil + } + return false, err + }) +} + +var _ = framework.ServiceCatalogDescribe("Instance", func() { + f := framework.NewDefaultFramework("instance") + + It("should verify an Instance can be deleted if referenced service class does not exist.", func() { + By("Creating an Instance") + instance := newTestInstance("test-instance", "no-service-class", "no-plan") + instance, err := createInstance(f.ServiceCatalogClientSet, f.Namespace.Name, instance) + Expect(err).NotTo(HaveOccurred()) + By("Deleting the Instance") + err = deleteInstance(f.ServiceCatalogClientSet, f.Namespace.Name, instance.Name) + Expect(err).NotTo(HaveOccurred()) + err = waitForInstanceToBeDeleted(f.ServiceCatalogClientSet, f.Namespace.Name, instance.Name) + Expect(err).NotTo(HaveOccurred()) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/util.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/util.go new file mode 100644 index 000000000000..009263954467 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/util.go @@ -0,0 +1,74 @@ +/* +Copyright 2017 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 e2e + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/pkg/api/v1" +) + +func NewUPSBrokerPod(name string) *v1.Pod { + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "app": name, + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: name, + Image: "quay.io/kubernetes-service-catalog/user-broker:latest", + Args: []string{ + "--port", + "8080", + }, + Ports: []v1.ContainerPort{ + { + ContainerPort: 8080, + }, + }, + }, + }, + }, + } +} + +func NewUPSBrokerService(name string) *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string { + "app": name, + }, + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": name, + }, + Ports: []v1.ServicePort{ + { + Protocol: v1.ProtocolTCP, + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/walkthrough.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/walkthrough.go new file mode 100644 index 000000000000..edc99b36e77e --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/e2e/walkthrough.go @@ -0,0 +1,209 @@ +/* +Copyright 2017 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 e2e + +import ( + v1alpha1 "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" + "github.com/kubernetes-incubator/service-catalog/test/e2e/framework" + "github.com/kubernetes-incubator/service-catalog/test/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/pkg/api/v1" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = framework.ServiceCatalogDescribe("walkthrough", func() { + f := framework.NewDefaultFramework("walkthrough-example") + + upsbrokername := "ups-broker" + + BeforeEach(func() { + //Deploying user provider service broker + By("Creating a user broker pod") + pod, err := f.KubeClientSet.CoreV1().Pods(f.Namespace.Name).Create(NewUPSBrokerPod(upsbrokername)) + Expect(err).NotTo(HaveOccurred(), "failed to create upsbroker pod") + + By("Waiting for pod to be running") + err = framework.WaitForPodRunningInNamespace(f.KubeClientSet, pod) + Expect(err).NotTo(HaveOccurred()) + + By("Createing a user provider broker service") + _, err = f.KubeClientSet.CoreV1().Services(f.Namespace.Name).Create(NewUPSBrokerService(upsbrokername)) + Expect(err).NotTo(HaveOccurred(), "failed to create upsbroker service") + }) + + AfterEach(func() { + //Deleting user provider service broker + By("Deleting the user provider broker pod") + err := f.KubeClientSet.CoreV1().Pods(f.Namespace.Name).Delete(upsbrokername, nil) + Expect(err).NotTo(HaveOccurred()) + + By("Deleting the upsbroker service") + err = f.KubeClientSet.CoreV1().Services(f.Namespace.Name).Delete(upsbrokername, nil) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Run walkthrough-example ", func() { + var ( + brokerName = upsbrokername + serviceclassName = "user-provided-service" + testns = "test-ns" + instanceName = "ups-instance" + bindingName = "ups-binding" + ) + + //Broker and ServiceClass should become ready + By("Make sure the named Broker not exist before create") + if _, err := f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Brokers().Get(brokerName, metav1.GetOptions{}); err == nil { + err = f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Brokers().Delete(brokerName, nil) + Expect(err).NotTo(HaveOccurred(), "failed to delete the broker") + + By("Waiting for Broker to not exist") + err = util.WaitForBrokerToNotExist(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), brokerName) + Expect(err).NotTo(HaveOccurred()) + } + + By("Creating a Broker") + url := "http://" + upsbrokername + "." +f.Namespace.Name + ".svc.cluster.local" + broker := &v1alpha1.Broker{ + ObjectMeta: metav1.ObjectMeta{ + Name: brokerName, + }, + Spec: v1alpha1.BrokerSpec{ + URL: url, + }, + } + broker, err := f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Brokers().Create(broker) + Expect(err).NotTo(HaveOccurred(), "failed to create Broker") + + By("Waiting for Broker to be ready") + err = util.WaitForBrokerCondition(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), + broker.Name, + v1alpha1.BrokerCondition{ + Type: v1alpha1.BrokerConditionReady, + Status: v1alpha1.ConditionTrue, + }, + ) + Expect(err).NotTo(HaveOccurred(), "failed to wait Broker to be ready") + + By("Waiting for ServiceClass to be ready") + err = util.WaitForServiceClassToExist(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), serviceclassName) + Expect(err).NotTo(HaveOccurred(), "failed to wait serviceclass to be ready") + + //Provisioning a Instance and binding to it + By("Creating a namespace") + testnamespace, err := framework.CreateKubeNamespace(testns, f.KubeClientSet) + Expect(err).NotTo(HaveOccurred(), "failed to create kube namespace") + + By("Creating a Instance") + instance := &v1alpha1.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: instanceName, + Namespace: testnamespace.Name, + }, + Spec: v1alpha1.InstanceSpec{ + ServiceClassName: serviceclassName, + PlanName: "default", + }, + } + instance, err = f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Instances(testnamespace.Name).Create(instance) + Expect(err).NotTo(HaveOccurred(), "failed to create instance") + + By("Waiting for Instance to be ready") + err = util.WaitForInstanceCondition(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), + testnamespace.Name, + instanceName, + v1alpha1.InstanceCondition{ + Type: v1alpha1.InstanceConditionReady, + Status: v1alpha1.ConditionTrue, + }, + ) + Expect(err).NotTo(HaveOccurred(), "failed to wait instance to be ready") + + //Binding to the Instance + By("Creating a Binding") + binding := &v1alpha1.Binding{ + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: testnamespace.Name, + }, + Spec: v1alpha1.BindingSpec{ + InstanceRef: v1.LocalObjectReference{ + Name: instanceName, + }, + SecretName: "my-secret", + }, + } + binding, err = f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Bindings(testnamespace.Name).Create(binding) + Expect(err).NotTo(HaveOccurred(), "failed to create binding") + + By("Waiting for Binding to be ready") + err = util.WaitForBindingCondition(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), + testnamespace.Name, + bindingName, + v1alpha1.BindingCondition{ + Type: v1alpha1.BindingConditionReady, + Status: v1alpha1.ConditionTrue, + }, + ) + Expect(err).NotTo(HaveOccurred(), "failed to wait binding to be ready") + + By("Secret should have been created after binding") + _, err = f.KubeClientSet.CoreV1().Secrets(testnamespace.Name).Get("my-secret", metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred(), "failed to create secret after binding") + + //Unbinding from the Instance + By("Deleting the Binding") + err = f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Bindings(testnamespace.Name).Delete(bindingName, nil) + Expect(err).NotTo(HaveOccurred(), "failed to delete the binding") + + By("Waiting for Binding to not exist") + err = util.WaitForBindingToNotExist(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), testnamespace.Name, bindingName) + Expect(err).NotTo(HaveOccurred()) + + By("Secret should been deleted after delete the binding") + _, err = f.KubeClientSet.CoreV1().Secrets(testnamespace.Name).Get("my-secret", metav1.GetOptions{}) + Expect(err).To(HaveOccurred()) + + //Deprovisioning the Instance + By("Deleting the Instance") + err = f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Instances(testnamespace.Name).Delete(instanceName, nil) + Expect(err).NotTo(HaveOccurred(), "failed to delete the instance") + + By("Waiting for Instance to not exist") + err = util.WaitForInstanceToNotExist(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), testnamespace.Name, instanceName) + Expect(err).NotTo(HaveOccurred()) + + By("Deleting the test namespace") + err = framework.DeleteKubeNamespace(f.KubeClientSet, testnamespace.Name) + Expect(err).NotTo(HaveOccurred()) + + //Deleting Broker and ServiceClass + By("Deleting the Broker") + err = f.ServiceCatalogClientSet.ServicecatalogV1alpha1().Brokers().Delete(brokerName, nil) + Expect(err).NotTo(HaveOccurred(), "failed to delete the broker") + + By("Waiting for Broker to not exist") + err = util.WaitForBrokerToNotExist(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), brokerName) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for ServiceClass to not exist") + err = util.WaitForServiceClassToNotExist(f.ServiceCatalogClientSet.ServicecatalogV1alpha1(), serviceclassName) + Expect(err).NotTo(HaveOccurred()) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/clientset_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/clientset_test.go index fd3628b139c3..17126733b364 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/clientset_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/clientset_test.go @@ -83,7 +83,9 @@ type bpStruct struct { func TestGroupVersion(t *testing.T) { rootTestFunc := func(sType server.StorageType) func(t *testing.T) { return func(t *testing.T) { - client, shutdownServer := getFreshApiserverAndClient(t, sType.String()) + client, shutdownServer := getFreshApiserverAndClient(t, sType.String(), func() runtime.Object { + return &servicecatalog.Broker{} + }) defer shutdownServer() if err := testGroupVersion(client); err != nil { t.Fatal(err) @@ -110,7 +112,9 @@ func testGroupVersion(client servicecatalogclient.Interface) error { func TestNoName(t *testing.T) { rootTestFunc := func(sType server.StorageType) func(t *testing.T) { return func(t *testing.T) { - client, shutdownServer := getFreshApiserverAndClient(t, sType.String()) + client, shutdownServer := getFreshApiserverAndClient(t, sType.String(), func() runtime.Object { + return &servicecatalog.Broker{} + }) defer shutdownServer() if err := testNoName(client); err != nil { t.Fatal(err) @@ -150,7 +154,9 @@ func TestBrokerClient(t *testing.T) { const name = "test-broker" rootTestFunc := func(sType server.StorageType) func(t *testing.T) { return func(t *testing.T) { - client, shutdownServer := getFreshApiserverAndClient(t, sType.String()) + client, shutdownServer := getFreshApiserverAndClient(t, sType.String(), func() runtime.Object { + return &servicecatalog.Broker{} + }) defer shutdownServer() if err := testBrokerClient(sType, client, name); err != nil { t.Fatal(err) @@ -224,12 +230,12 @@ func testBrokerClient(sType server.StorageType, client servicecatalogclient.Inte Name: "test-name", } - brokerServer.Spec.AuthSecret = authSecret + brokerServer.Spec.AuthInfo = &v1alpha1.BrokerAuthInfo{BasicAuthSecret: authSecret} brokerUpdated, err := brokerClient.Update(brokerServer) if nil != err || - "test-namespace" != brokerUpdated.Spec.AuthSecret.Namespace || - "test-name" != brokerUpdated.Spec.AuthSecret.Name { + "test-namespace" != brokerUpdated.Spec.AuthInfo.BasicAuthSecret.Namespace || + "test-name" != brokerUpdated.Spec.AuthInfo.BasicAuthSecret.Name { return fmt.Errorf("broker wasn't updated, %v, %v", brokerServer, brokerUpdated) } @@ -272,8 +278,8 @@ func testBrokerClient(sType server.StorageType, client servicecatalogclient.Inte brokerServer, err = brokerClient.Get(name, metav1.GetOptions{}) if nil != err || - "test-namespace" != brokerServer.Spec.AuthSecret.Namespace || - "test-name" != brokerServer.Spec.AuthSecret.Name { + "test-namespace" != brokerServer.Spec.AuthInfo.BasicAuthSecret.Namespace || + "test-name" != brokerServer.Spec.AuthInfo.BasicAuthSecret.Name { return fmt.Errorf("broker wasn't updated (%v)", brokerServer) } if e, a := readyConditionFalse, brokerServer.Status.Conditions[0]; !reflect.DeepEqual(e, a) { @@ -308,7 +314,9 @@ func TestServiceClassClient(t *testing.T) { rootTestFunc := func(sType server.StorageType) func(t *testing.T) { return func(t *testing.T) { const name = "test-serviceclass" - client, shutdownServer := getFreshApiserverAndClient(t, sType.String()) + client, shutdownServer := getFreshApiserverAndClient(t, sType.String(), func() runtime.Object { + return &servicecatalog.ServiceClass{} + }) defer shutdownServer() if err := testServiceClassClient(sType, client, name); err != nil { @@ -332,6 +340,13 @@ func testServiceClassClient(sType server.StorageType, client servicecatalogclien Bindable: true, ExternalID: "b8269ab4-7d2d-456d-8c8b-5aab63b321d1", Description: "test description", + Plans: []v1alpha1.ServicePlan{ + { + Name: "test-service-plan", + ExternalID: "test-service-plan-external-id", + Description: "test-description", + }, + }, } // start from scratch @@ -422,7 +437,9 @@ func TestInstanceClient(t *testing.T) { rootTestFunc := func(sType server.StorageType) func(t *testing.T) { return func(t *testing.T) { const name = "test-instance" - client, shutdownServer := getFreshApiserverAndClient(t, sType.String()) + client, shutdownServer := getFreshApiserverAndClient(t, sType.String(), func() runtime.Object { + return &servicecatalog.Instance{} + }) defer shutdownServer() if err := testInstanceClient(sType, client, name); err != nil { t.Fatal(err) @@ -582,7 +599,9 @@ func TestBindingClient(t *testing.T) { rootTestFunc := func(sType server.StorageType) func(t *testing.T) { return func(t *testing.T) { const name = "test-binding" - client, shutdownServer := getFreshApiserverAndClient(t, sType.String()) + client, shutdownServer := getFreshApiserverAndClient(t, sType.String(), func() runtime.Object { + return &servicecatalog.Binding{} + }) defer shutdownServer() if err := testBindingClient(sType, client, name); err != nil { @@ -717,23 +736,25 @@ func testBindingClient(sType server.StorageType, client servicecatalogclient.Int } if err = bindingClient.Delete(name, &metav1.DeleteOptions{}); nil != err { - return fmt.Errorf("broker should be deleted (%v)", err) + return fmt.Errorf("binding delete failed (%s)", err) } bindingDeleted, err := bindingClient.Get(name, metav1.GetOptions{}) if nil != err { - return fmt.Errorf("binding should still exist (%v): %v", bindingDeleted, err) + return fmt.Errorf("binding should still exist on initial get (%s)", err) } + fmt.Printf("-----\nclientset_test\n\nbinding deleted: %#v\n\n", *bindingDeleted) bindingDeleted.ObjectMeta.Finalizers = nil - _, err = bindingClient.UpdateStatus(bindingDeleted) - if nil != err { - return fmt.Errorf("error updating status (%v): %v", bindingDeleted, err) + if _, err := bindingClient.UpdateStatus(bindingDeleted); err != nil { + return fmt.Errorf("error updating binding status (%s)", err) } - bindingDeleted, err = bindingClient.Get(name, metav1.GetOptions{}) - if nil == err { - return fmt.Errorf("binding should be deleted (%#v)", bindingDeleted) + if bindingDeleted, err := bindingClient.Get(name, metav1.GetOptions{}); err == nil { + return fmt.Errorf( + "binding should be deleted after finalizers cleared. got binding %#v", + *bindingDeleted, + ) } return nil } diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/controller_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/controller_test.go index ac56e5a47096..bd7486b83790 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/controller_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/controller_test.go @@ -30,6 +30,7 @@ import ( // avoid error `no kind is registered for the type metav1.ListOptions` _ "k8s.io/client-go/pkg/api/install" + "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog" "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1alpha1" "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi" fakebrokerapi "github.com/kubernetes-incubator/service-catalog/pkg/brokerapi/fake" @@ -39,6 +40,7 @@ import ( "github.com/kubernetes-incubator/service-catalog/pkg/controller" "github.com/kubernetes-incubator/service-catalog/pkg/registry/servicecatalog/server" "github.com/kubernetes-incubator/service-catalog/test/util" + "k8s.io/apimachinery/pkg/runtime" ) const ( @@ -238,7 +240,9 @@ func newTestController(t *testing.T) ( // create a fake kube client fakeKubeClient := &fake.Clientset{} // create an sc client and running server - catalogClient, shutdownServer := getFreshApiserverAndClient(t, server.StorageTypeEtcd.String()) + catalogClient, shutdownServer := getFreshApiserverAndClient(t, server.StorageTypeEtcd.String(), func() runtime.Object { + return &servicecatalog.Broker{} + }) catalogCl := &fakebrokerapi.CatalogClient{} instanceCl := fakebrokerapi.NewInstanceClient() diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/framework.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/framework.go index 37f48f23590b..8a4423b7a13d 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/framework.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/test/integration/framework.go @@ -39,6 +39,7 @@ import ( "github.com/kubernetes-incubator/service-catalog/cmd/apiserver/app/server" _ "github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/install" servicecatalogclient "github.com/kubernetes-incubator/service-catalog/pkg/client/clientset_generated/clientset" + "k8s.io/apimachinery/pkg/runtime" _ "k8s.io/client-go/pkg/api/install" _ "k8s.io/client-go/pkg/apis/extensions/install" ) @@ -51,7 +52,11 @@ func init() { rand.Seed(time.Now().UnixNano()) } -func getFreshApiserverAndClient(t *testing.T, storageTypeStr string) (servicecatalogclient.Interface, func()) { +func getFreshApiserverAndClient( + t *testing.T, + storageTypeStr string, + newEmptyObj func() runtime.Object, +) (servicecatalogclient.Interface, func()) { securePort := rand.Intn(31743) + 1024 insecurePort := rand.Intn(31743) + 1024 insecureAddr := fmt.Sprintf("http://localhost:%d", insecurePort) @@ -68,7 +73,7 @@ func getFreshApiserverAndClient(t *testing.T, storageTypeStr string) (servicecat go func() { tprOptions := server.NewTPROptions() - tprOptions.RESTClient = fake.NewRESTClient() + tprOptions.RESTClient = fake.NewRESTClient(newEmptyObj) tprOptions.InstallTPRsFunc = func() error { return nil } @@ -84,6 +89,7 @@ func getFreshApiserverAndClient(t *testing.T, storageTypeStr string) (servicecat TPROptions: tprOptions, AuthenticationOptions: genericserveroptions.NewDelegatingAuthenticationOptions(), AuthorizationOptions: genericserveroptions.NewDelegatingAuthorizationOptions(), + AuditOptions: genericserveroptions.NewAuditLogOptions(), DisableAuth: true, StopCh: stopCh, StandaloneMode: true, // this must be true because we have no kube server for integration. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt index 3c3827ab7d16..b9a2ff984570 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/bitbucket.org/ww/goautoneg/.hg_archival.txt @@ -3,3 +3,4 @@ node: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 branch: default latesttag: null latesttagdistance: 5 +changessincelatesttag: 5 diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/LICENSE b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/LICENSE new file mode 100644 index 000000000000..5c304d1a4a7b --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/NOTICE b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/NOTICE new file mode 100644 index 000000000000..ff96b880bdd1 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/NOTICE @@ -0,0 +1,15 @@ +lager + +Copyright (c) 2014-Present CloudFoundry.org Foundation, 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. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/README.md b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/README.md new file mode 100644 index 000000000000..c9f28cc6db09 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/README.md @@ -0,0 +1,78 @@ +lager +===== + +**Note**: This repository should be imported as `code.cloudfoundry.org/lager`. + +Lager is a logging library for go. + +## Usage + +Instantiate a logger with the name of your component. + +```go +import ( + "code.cloudfoundry.org/lager" +) + +logger := lager.NewLogger("my-app") +``` + +### Sinks + +Lager can write logs to a variety of destinations. You can specify the destinations +using Lager sinks: + +To write to an arbitrary `Writer` object: + +```go +logger.RegisterSink(lager.NewWriterSink(myWriter, lager.INFO)) +``` + +### Emitting logs + +Lager supports the usual level-based logging, with an optional argument for arbitrary key-value data. + +```go +logger.Info("doing-stuff", lager.Data{ + "informative": true, +}) +``` + +output: +```json +{ "source": "my-app", "message": "doing-stuff", "data": { "informative": true }, "timestamp": 1232345, "log_level": 1 } +``` + +Error messages also take an `Error` object: + +```go +logger.Error("failed-to-do-stuff", errors.New("Something went wrong")) +``` + +output: +```json +{ "source": "my-app", "message": "failed-to-do-stuff", "data": { "error": "Something went wrong" }, "timestamp": 1232345, "log_level": 1 } +``` + +### Sessions + +You can avoid repetition of contextual data using 'Sessions': + +```go + +contextualLogger := logger.Session("my-task", lager.Data{ + "request-id": 5, +}) + +contextualLogger.Info("my-action") +``` + +output: + +```json +{ "source": "my-app", "message": "my-task.my-action", "data": { "request-id": 5 }, "timestamp": 1232345, "log_level": 1 } +``` + +## License + +Lager is [Apache 2.0](https://github.com/cloudfoundry/lager/blob/master/LICENSE) licensed. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug.go new file mode 100644 index 000000000000..80672fbcb9a4 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug.go @@ -0,0 +1,130 @@ +package chug + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "io" + "strconv" + "strings" + "time" + + "code.cloudfoundry.org/lager" +) + +type Entry struct { + IsLager bool + Raw []byte + Log LogEntry +} + +type LogEntry struct { + Timestamp time.Time + LogLevel lager.LogLevel + + Source string + Message string + Session string + + Error error + Trace string + + Data lager.Data +} + +func Chug(reader io.Reader, out chan<- Entry) { + scanner := bufio.NewReader(reader) + for { + line, err := scanner.ReadBytes('\n') + if line != nil { + out <- entry(bytes.TrimSuffix(line, []byte{'\n'})) + } + if err != nil { + break + } + } + close(out) +} + +func entry(raw []byte) (entry Entry) { + copiedBytes := make([]byte, len(raw)) + copy(copiedBytes, raw) + entry = Entry{ + IsLager: false, + Raw: copiedBytes, + } + + rawString := string(raw) + idx := strings.Index(rawString, "{") + if idx == -1 { + return + } + + var lagerLog lager.LogFormat + decoder := json.NewDecoder(strings.NewReader(rawString[idx:])) + err := decoder.Decode(&lagerLog) + if err != nil { + return + } + + entry.Log, entry.IsLager = convertLagerLog(lagerLog) + + return +} + +func convertLagerLog(lagerLog lager.LogFormat) (LogEntry, bool) { + timestamp, err := strconv.ParseFloat(lagerLog.Timestamp, 64) + + if err != nil { + return LogEntry{}, false + } + + data := lagerLog.Data + + var logErr error + if lagerLog.LogLevel == lager.ERROR || lagerLog.LogLevel == lager.FATAL { + dataErr, ok := lagerLog.Data["error"] + if ok { + errorString, ok := dataErr.(string) + if !ok { + return LogEntry{}, false + } + logErr = errors.New(errorString) + delete(lagerLog.Data, "error") + } + } + + var logTrace string + dataTrace, ok := lagerLog.Data["trace"] + if ok { + logTrace, ok = dataTrace.(string) + if !ok { + return LogEntry{}, false + } + delete(lagerLog.Data, "trace") + } + + var logSession string + dataSession, ok := lagerLog.Data["session"] + if ok { + logSession, ok = dataSession.(string) + if !ok { + return LogEntry{}, false + } + delete(lagerLog.Data, "session") + } + + return LogEntry{ + Timestamp: time.Unix(0, int64(timestamp*1e9)), + LogLevel: lagerLog.LogLevel, + Source: lagerLog.Source, + Message: lagerLog.Message, + Session: logSession, + + Error: logErr, + Trace: logTrace, + + Data: data, + }, true +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug_suite_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug_suite_test.go new file mode 100644 index 000000000000..46cc34c22e77 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug_suite_test.go @@ -0,0 +1,13 @@ +package chug_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestChug(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Chug Suite") +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug_test.go new file mode 100644 index 000000000000..7c262316579a --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/chug_test.go @@ -0,0 +1,247 @@ +package chug_test + +import ( + "errors" + "io" + "time" + + "code.cloudfoundry.org/lager" + . "code.cloudfoundry.org/lager/chug" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Chug", func() { + var ( + logger lager.Logger + stream chan Entry + pipeReader *io.PipeReader + pipeWriter *io.PipeWriter + ) + + BeforeEach(func() { + pipeReader, pipeWriter = io.Pipe() + logger = lager.NewLogger("chug-test") + logger.RegisterSink(lager.NewWriterSink(pipeWriter, lager.DEBUG)) + stream = make(chan Entry, 100) + go Chug(pipeReader, stream) + }) + + AfterEach(func() { + pipeWriter.Close() + Eventually(stream).Should(BeClosed()) + }) + + Context("when fed a stream of well-formed lager messages", func() { + It("should return parsed lager messages", func() { + data := lager.Data{"some-float": 3.0, "some-string": "foo"} + logger.Debug("chug", data) + logger.Info("again", data) + + entry := <-stream + Expect(entry.IsLager).To(BeTrue()) + Expect(entry.Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.DEBUG, + Source: "chug-test", + Message: "chug-test.chug", + Data: data, + })) + + entry = <-stream + Expect(entry.IsLager).To(BeTrue()) + Expect(entry.Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.again", + Data: data, + })) + + }) + + It("should parse the timestamp", func() { + logger.Debug("chug") + entry := <-stream + Expect(entry.Log.Timestamp).To(BeTemporally("~", time.Now(), 10*time.Millisecond)) + }) + + Context("when parsing an error message", func() { + It("should include the error", func() { + data := lager.Data{"some-float": 3.0, "some-string": "foo"} + logger.Error("chug", errors.New("some-error"), data) + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.ERROR, + Source: "chug-test", + Message: "chug-test.chug", + Error: errors.New("some-error"), + Data: lager.Data{"some-float": 3.0, "some-string": "foo"}, + })) + + }) + }) + + Context("when parsing an info message with an error", func() { + It("should not take the error out of the data map", func() { + data := lager.Data{"some-float": 3.0, "some-string": "foo", "error": "some-error"} + logger.Info("chug", data) + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.chug", + Error: nil, + Data: lager.Data{"some-float": 3.0, "some-string": "foo", "error": "some-error"}, + })) + + }) + }) + + Context("when multiple sessions have been established", func() { + It("should build up the task array appropriately", func() { + firstSession := logger.Session("first-session") + firstSession.Info("encabulate") + nestedSession := firstSession.Session("nested-session-1") + nestedSession.Info("baconize") + firstSession.Info("remodulate") + nestedSession.Info("ergonomize") + nestedSession = firstSession.Session("nested-session-2") + nestedSession.Info("modernify") + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.encabulate", + Session: "1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.nested-session-1.baconize", + Session: "1.1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.remodulate", + Session: "1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.nested-session-1.ergonomize", + Session: "1.1", + Data: lager.Data{}, + })) + + Expect((<-stream).Log).To(MatchLogEntry(LogEntry{ + LogLevel: lager.INFO, + Source: "chug-test", + Message: "chug-test.first-session.nested-session-2.modernify", + Session: "1.2", + Data: lager.Data{}, + })) + + }) + }) + }) + + Context("handling lager JSON that is surrounded by non-JSON", func() { + var input []byte + var entry Entry + + BeforeEach(func() { + input = []byte(`[some-component][e]{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":0,"data":{"some-float":3,"some-string":"foo"}}...some trailing stuff`) + pipeWriter.Write(input) + pipeWriter.Write([]byte("\n")) + + Eventually(stream).Should(Receive(&entry)) + }) + + It("should be a lager message", func() { + Expect(entry.IsLager).To(BeTrue()) + }) + + It("should contain all the data in Raw", func() { + Expect(entry.Raw).To(Equal(input)) + }) + + It("should succesfully parse the lager message", func() { + Expect(entry.Log.Source).To(Equal("chug-test")) + }) + }) + + Context("handling malformed/non-lager data", func() { + var input []byte + var entry Entry + + JustBeforeEach(func() { + pipeWriter.Write(input) + pipeWriter.Write([]byte("\n")) + + Eventually(stream).Should(Receive(&entry)) + }) + + itReturnsRawData := func() { + It("returns raw data", func() { + Expect(entry.IsLager).To(BeFalse()) + Expect(entry.Log).To(BeZero()) + Expect(entry.Raw).To(Equal(input)) + }) + } + + Context("when fed a stream of malformed lager messages", func() { + Context("when the timestamp is invalid", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"tomorrow","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","error":7}}`) + }) + + itReturnsRawData() + }) + + Context("when the error does not parse", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","error":7}}`) + }) + + itReturnsRawData() + }) + + Context("when the trace does not parse", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","trace":7}}`) + }) + + itReturnsRawData() + }) + + Context("when the session does not parse", func() { + BeforeEach(func() { + input = []byte(`{"timestamp":"1407102779.028711081","source":"chug-test","message":"chug-test.chug","log_level":3,"data":{"some-float":3,"some-string":"foo","session":7}}`) + }) + + itReturnsRawData() + }) + }) + + Context("When fed JSON that is not a lager message at all", func() { + BeforeEach(func() { + input = []byte(`{"source":"chattanooga"}`) + }) + + itReturnsRawData() + }) + + Context("When fed none-JSON that is not a lager message at all", func() { + BeforeEach(func() { + input = []byte(`ß`) + }) + + itReturnsRawData() + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/match_log_entry_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/match_log_entry_test.go new file mode 100644 index 000000000000..03d6c77b308c --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/chug/match_log_entry_test.go @@ -0,0 +1,41 @@ +package chug_test + +import ( + "fmt" + "reflect" + + "code.cloudfoundry.org/lager/chug" + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +func MatchLogEntry(entry chug.LogEntry) types.GomegaMatcher { + return &logEntryMatcher{entry} +} + +type logEntryMatcher struct { + entry chug.LogEntry +} + +func (m *logEntryMatcher) Match(actual interface{}) (success bool, err error) { + actualEntry, ok := actual.(chug.LogEntry) + if !ok { + return false, fmt.Errorf("MatchLogEntry must be passed a chug.LogEntry. Got:\n%s", format.Object(actual, 1)) + } + + return m.entry.LogLevel == actualEntry.LogLevel && + m.entry.Source == actualEntry.Source && + m.entry.Message == actualEntry.Message && + m.entry.Session == actualEntry.Session && + reflect.DeepEqual(m.entry.Error, actualEntry.Error) && + m.entry.Trace == actualEntry.Trace && + reflect.DeepEqual(m.entry.Data, actualEntry.Data), nil +} + +func (m *logEntryMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to equal", m.entry) +} + +func (m *logEntryMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to equal", m.entry) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgo_reporter.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgo_reporter.go new file mode 100644 index 000000000000..00b7b8f14ae0 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgo_reporter.go @@ -0,0 +1,155 @@ +package ginkgoreporter + +import ( + "fmt" + "io" + "time" + + "code.cloudfoundry.org/lager" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +type SuiteStartSummary struct { + RandomSeed int64 `json:"random_seed"` + SuiteDescription string `json:"description"` + NumberOfSpecsThatWillBeRun int `json:"num_specs"` +} + +type SuiteEndSummary struct { + SuiteDescription string `json:"description"` + Passed bool + NumberOfSpecsThatWillBeRun int `json:"num_specs"` + NumberOfPassedSpecs int `json:"num_passed"` + NumberOfFailedSpecs int `json:"num_failed"` +} + +type SpecSummary struct { + Name []string `json:"name"` + Location string `json:"location"` + State string `json:"state"` + Passed bool `json:"passed"` + RunTime time.Duration `json:"run_time"` + + StackTrace string `json:"stack_trace,omitempty"` +} + +type SetupSummary struct { + Name string `json:"name"` + State string `json:"state"` + Passed bool `json:"passed"` + RunTime time.Duration `json:"run_time,omitempty"` + + StackTrace string `json:"stack_trace,omitempty"` +} + +func New(writer io.Writer) *GinkgoReporter { + logger := lager.NewLogger("ginkgo") + logger.RegisterSink(lager.NewWriterSink(writer, lager.DEBUG)) + return &GinkgoReporter{ + writer: writer, + logger: logger, + } +} + +type GinkgoReporter struct { + logger lager.Logger + writer io.Writer + session lager.Logger +} + +func (g *GinkgoReporter) wrappedWithNewlines(f func()) { + g.writer.Write([]byte("\n")) + f() + g.writer.Write([]byte("\n")) +} + +func (g *GinkgoReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + if config.ParallelTotal > 1 { + var session = g.logger + for i := 0; i < config.ParallelNode; i++ { + session = g.logger.Session(fmt.Sprintf("node-%d", i+1)) + } + g.logger = session + } +} + +func (g *GinkgoReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { +} + +func (g *GinkgoReporter) SpecWillRun(specSummary *types.SpecSummary) { + g.wrappedWithNewlines(func() { + g.session = g.logger.Session("spec") + g.session.Info("start", lager.Data{ + "summary": SpecSummary{ + Name: specSummary.ComponentTexts, + Location: specSummary.ComponentCodeLocations[len(specSummary.ComponentTexts)-1].String(), + }, + }) + }) +} + +func (g *GinkgoReporter) SpecDidComplete(specSummary *types.SpecSummary) { + g.wrappedWithNewlines(func() { + if g.session == nil { + return + } + summary := SpecSummary{ + Name: specSummary.ComponentTexts, + Location: specSummary.ComponentCodeLocations[len(specSummary.ComponentTexts)-1].String(), + State: stateAsString(specSummary.State), + Passed: passed(specSummary.State), + RunTime: specSummary.RunTime, + } + + if passed(specSummary.State) { + g.session.Info("end", lager.Data{ + "summary": summary, + }) + } else { + summary.StackTrace = specSummary.Failure.Location.FullStackTrace + g.session.Error("end", errorForFailure(specSummary.Failure), lager.Data{ + "summary": summary, + }) + } + g.session = nil + }) +} + +func (g *GinkgoReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { +} + +func (g *GinkgoReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { +} + +func stateAsString(state types.SpecState) string { + switch state { + case types.SpecStatePending: + return "PENDING" + case types.SpecStateSkipped: + return "SKIPPED" + case types.SpecStatePassed: + return "PASSED" + case types.SpecStateFailed: + return "FAILED" + case types.SpecStatePanicked: + return "PANICKED" + case types.SpecStateTimedOut: + return "TIMED OUT" + default: + return "INVALID" + } +} + +func passed(state types.SpecState) bool { + return !(state == types.SpecStateFailed || state == types.SpecStatePanicked || state == types.SpecStateTimedOut) +} + +func errorForFailure(failure types.SpecFailure) error { + message := failure.Message + if failure.ForwardedPanic != "" { + message += fmt.Sprintf("%s", failure.ForwardedPanic) + } + + return fmt.Errorf("%s\n%s", message, failure.Location.String()) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_suite_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_suite_test.go new file mode 100644 index 000000000000..fa079140827f --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_suite_test.go @@ -0,0 +1,13 @@ +package ginkgoreporter_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGinkgoReporter(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "GinkgoReporter Suite") +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_test.go new file mode 100644 index 000000000000..49cde07318bd --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/ginkgoreporter/ginkgoreporter_test.go @@ -0,0 +1,185 @@ +package ginkgoreporter_test + +import ( + "bytes" + "encoding/json" + "time" + + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/chug" + . "code.cloudfoundry.org/lager/ginkgoreporter" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ginkgoreporter", func() { + var ( + reporter reporters.Reporter + buffer *bytes.Buffer + ) + + BeforeEach(func() { + buffer = &bytes.Buffer{} + reporter = New(buffer) + }) + + fetchLogs := func() []chug.LogEntry { + out := make(chan chug.Entry, 1000) + chug.Chug(buffer, out) + logs := []chug.LogEntry{} + for entry := range out { + if entry.IsLager { + logs = append(logs, entry.Log) + } + } + return logs + } + + jsonRoundTrip := func(object interface{}) interface{} { + jsonEncoded, err := json.Marshal(object) + Expect(err).NotTo(HaveOccurred()) + var out interface{} + err = json.Unmarshal(jsonEncoded, &out) + Expect(err).NotTo(HaveOccurred()) + return out + } + + Describe("Announcing specs", func() { + var summary *types.SpecSummary + BeforeEach(func() { + summary = &types.SpecSummary{ + ComponentTexts: []string{"A", "B"}, + ComponentCodeLocations: []types.CodeLocation{ + { + FileName: "file/a", + LineNumber: 3, + FullStackTrace: "some-stack-trace", + }, + { + FileName: "file/b", + LineNumber: 4, + FullStackTrace: "some-stack-trace", + }, + }, + RunTime: time.Minute, + State: types.SpecStatePassed, + } + }) + + Context("when running in parallel", func() { + It("should include the node # in the session and message", func() { + configType := config.GinkgoConfigType{ + ParallelTotal: 3, + ParallelNode: 2, + } + suiteSummary := &types.SuiteSummary{} + reporter.SpecSuiteWillBegin(configType, suiteSummary) + + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + + logs := fetchLogs() + Expect(logs[0].Session).To(Equal("2.1")) + Expect(logs[0].Message).To(Equal("ginkgo.node-2.spec.start")) + Expect(logs[1].Session).To(Equal("2.1")) + Expect(logs[1].Message).To(Equal("ginkgo.node-2.spec.end")) + Expect(logs[2].Session).To(Equal("2.2")) + Expect(logs[0].Message).To(Equal("ginkgo.node-2.spec.start")) + Expect(logs[3].Session).To(Equal("2.2")) + Expect(logs[1].Message).To(Equal("ginkgo.node-2.spec.end")) + }) + }) + + Describe("incrementing sessions", func() { + It("should increment the session counter as specs run", func() { + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + reporter.SpecWillRun(summary) + reporter.SpecDidComplete(summary) + + logs := fetchLogs() + Expect(logs[0].Session).To(Equal("1")) + Expect(logs[1].Session).To(Equal("1")) + Expect(logs[2].Session).To(Equal("2")) + Expect(logs[3].Session).To(Equal("2")) + }) + }) + + Context("when a spec starts", func() { + BeforeEach(func() { + reporter.SpecWillRun(summary) + }) + + It("should log about the spec starting", func() { + log := fetchLogs()[0] + Expect(log.LogLevel).To(Equal(lager.INFO)) + Expect(log.Source).To(Equal("ginkgo")) + Expect(log.Message).To(Equal("ginkgo.spec.start")) + Expect(log.Session).To(Equal("1")) + Expect(log.Data["summary"]).To(Equal(jsonRoundTrip(SpecSummary{ + Name: []string{"A", "B"}, + Location: "file/b:4", + }))) + + }) + + Context("when the spec succeeds", func() { + It("should info", func() { + reporter.SpecDidComplete(summary) + log := fetchLogs()[1] + Expect(log.LogLevel).To(Equal(lager.INFO)) + Expect(log.Source).To(Equal("ginkgo")) + Expect(log.Message).To(Equal("ginkgo.spec.end")) + Expect(log.Session).To(Equal("1")) + Expect(log.Data["summary"]).To(Equal(jsonRoundTrip(SpecSummary{ + Name: []string{"A", "B"}, + Location: "file/b:4", + State: "PASSED", + Passed: true, + RunTime: time.Minute, + }))) + + }) + }) + + Context("when the spec fails", func() { + BeforeEach(func() { + summary.State = types.SpecStateFailed + summary.Failure = types.SpecFailure{ + Message: "something failed!", + Location: types.CodeLocation{ + FileName: "some/file", + LineNumber: 3, + FullStackTrace: "some-stack-trace", + }, + } + }) + + It("should error", func() { + reporter.SpecDidComplete(summary) + log := fetchLogs()[1] + Expect(log.LogLevel).To(Equal(lager.ERROR)) + Expect(log.Source).To(Equal("ginkgo")) + Expect(log.Message).To(Equal("ginkgo.spec.end")) + Expect(log.Session).To(Equal("1")) + Expect(log.Error.Error()).To(Equal("something failed!\nsome/file:3")) + Expect(log.Data["summary"]).To(Equal(jsonRoundTrip(SpecSummary{ + Name: []string{"A", "B"}, + Location: "file/b:4", + State: "FAILED", + Passed: false, + RunTime: time.Minute, + StackTrace: "some-stack-trace", + }))) + + }) + }) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/lager_suite_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/lager_suite_test.go new file mode 100644 index 000000000000..b7670a7f1b42 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/lager_suite_test.go @@ -0,0 +1,13 @@ +package lager_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLager(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Lager Suite") +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/lagertest/test_sink.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/lagertest/test_sink.go new file mode 100644 index 000000000000..79782ab05040 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/lagertest/test_sink.go @@ -0,0 +1,71 @@ +package lagertest + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega/gbytes" + + "code.cloudfoundry.org/lager" +) + +type TestLogger struct { + lager.Logger + *TestSink +} + +type TestSink struct { + lager.Sink + buffer *gbytes.Buffer +} + +func NewTestLogger(component string) *TestLogger { + logger := lager.NewLogger(component) + + testSink := NewTestSink() + logger.RegisterSink(testSink) + logger.RegisterSink(lager.NewWriterSink(ginkgo.GinkgoWriter, lager.DEBUG)) + + return &TestLogger{logger, testSink} +} + +func NewTestSink() *TestSink { + buffer := gbytes.NewBuffer() + + return &TestSink{ + Sink: lager.NewWriterSink(buffer, lager.DEBUG), + buffer: buffer, + } +} + +func (s *TestSink) Buffer() *gbytes.Buffer { + return s.buffer +} + +func (s *TestSink) Logs() []lager.LogFormat { + logs := []lager.LogFormat{} + + decoder := json.NewDecoder(bytes.NewBuffer(s.buffer.Contents())) + for { + var log lager.LogFormat + if err := decoder.Decode(&log); err == io.EOF { + return logs + } else if err != nil { + panic(err) + } + logs = append(logs, log) + } + + return logs +} + +func (s *TestSink) LogMessages() []string { + logs := s.Logs() + messages := make([]string, 0, len(logs)) + for _, log := range logs { + messages = append(messages, log.Message) + } + return messages +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/logger.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/logger.go new file mode 100644 index 000000000000..70727655a65c --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/logger.go @@ -0,0 +1,179 @@ +package lager + +import ( + "fmt" + "runtime" + "sync/atomic" + "time" +) + +const StackTraceBufferSize = 1024 * 100 + +type Logger interface { + RegisterSink(Sink) + Session(task string, data ...Data) Logger + SessionName() string + Debug(action string, data ...Data) + Info(action string, data ...Data) + Error(action string, err error, data ...Data) + Fatal(action string, err error, data ...Data) + WithData(Data) Logger +} + +type logger struct { + component string + task string + sinks []Sink + sessionID string + nextSession uint32 + data Data +} + +func NewLogger(component string) Logger { + return &logger{ + component: component, + task: component, + sinks: []Sink{}, + data: Data{}, + } +} + +func (l *logger) RegisterSink(sink Sink) { + l.sinks = append(l.sinks, sink) +} + +func (l *logger) SessionName() string { + return l.task +} + +func (l *logger) Session(task string, data ...Data) Logger { + sid := atomic.AddUint32(&l.nextSession, 1) + + var sessionIDstr string + + if l.sessionID != "" { + sessionIDstr = fmt.Sprintf("%s.%d", l.sessionID, sid) + } else { + sessionIDstr = fmt.Sprintf("%d", sid) + } + + return &logger{ + component: l.component, + task: fmt.Sprintf("%s.%s", l.task, task), + sinks: l.sinks, + sessionID: sessionIDstr, + data: l.baseData(data...), + } +} + +func (l *logger) WithData(data Data) Logger { + return &logger{ + component: l.component, + task: l.task, + sinks: l.sinks, + sessionID: l.sessionID, + data: l.baseData(data), + } +} + +func (l *logger) Debug(action string, data ...Data) { + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: DEBUG, + Data: l.baseData(data...), + } + + for _, sink := range l.sinks { + sink.Log(log) + } +} + +func (l *logger) Info(action string, data ...Data) { + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: INFO, + Data: l.baseData(data...), + } + + for _, sink := range l.sinks { + sink.Log(log) + } +} + +func (l *logger) Error(action string, err error, data ...Data) { + logData := l.baseData(data...) + + if err != nil { + logData["error"] = err.Error() + } + + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: ERROR, + Data: logData, + } + + for _, sink := range l.sinks { + sink.Log(log) + } +} + +func (l *logger) Fatal(action string, err error, data ...Data) { + logData := l.baseData(data...) + + stackTrace := make([]byte, StackTraceBufferSize) + stackSize := runtime.Stack(stackTrace, false) + stackTrace = stackTrace[:stackSize] + + if err != nil { + logData["error"] = err.Error() + } + + logData["trace"] = string(stackTrace) + + log := LogFormat{ + Timestamp: currentTimestamp(), + Source: l.component, + Message: fmt.Sprintf("%s.%s", l.task, action), + LogLevel: FATAL, + Data: logData, + } + + for _, sink := range l.sinks { + sink.Log(log) + } + + panic(err) +} + +func (l *logger) baseData(givenData ...Data) Data { + data := Data{} + + for k, v := range l.data { + data[k] = v + } + + if len(givenData) > 0 { + for _, dataArg := range givenData { + for key, val := range dataArg { + data[key] = val + } + } + } + + if l.sessionID != "" { + data["session"] = l.sessionID + } + + return data +} + +func currentTimestamp() string { + return fmt.Sprintf("%.9f", float64(time.Now().UnixNano())/1e9) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/logger_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/logger_test.go new file mode 100644 index 000000000000..1d7e173addca --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/logger_test.go @@ -0,0 +1,358 @@ +package lager_test + +import ( + "errors" + "fmt" + "strconv" + "time" + + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/lagertest" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Logger", func() { + var logger lager.Logger + var testSink *lagertest.TestSink + + var component = "my-component" + var action = "my-action" + var logData = lager.Data{ + "foo": "bar", + "a-number": 7, + } + var anotherLogData = lager.Data{ + "baz": "quux", + "b-number": 43, + } + + BeforeEach(func() { + logger = lager.NewLogger(component) + testSink = lagertest.NewTestSink() + logger.RegisterSink(testSink) + }) + + var TestCommonLogFeatures = func(level lager.LogLevel) { + var log lager.LogFormat + + BeforeEach(func() { + log = testSink.Logs()[0] + }) + + It("writes a log to the sink", func() { + Expect(testSink.Logs()).To(HaveLen(1)) + }) + + It("records the source component", func() { + Expect(log.Source).To(Equal(component)) + }) + + It("outputs a properly-formatted message", func() { + Expect(log.Message).To(Equal(fmt.Sprintf("%s.%s", component, action))) + }) + + It("has a timestamp", func() { + expectedTime := float64(time.Now().UnixNano()) / 1e9 + parsedTimestamp, err := strconv.ParseFloat(log.Timestamp, 64) + Expect(err).NotTo(HaveOccurred()) + Expect(parsedTimestamp).To(BeNumerically("~", expectedTime, 1.0)) + }) + + It("sets the proper output level", func() { + Expect(log.LogLevel).To(Equal(level)) + }) + } + + var TestLogData = func() { + var log lager.LogFormat + + BeforeEach(func() { + log = testSink.Logs()[0] + }) + + It("data contains custom user data", func() { + Expect(log.Data["foo"]).To(Equal("bar")) + Expect(log.Data["a-number"]).To(BeNumerically("==", 7)) + Expect(log.Data["baz"]).To(Equal("quux")) + Expect(log.Data["b-number"]).To(BeNumerically("==", 43)) + }) + } + + Describe("Session", func() { + var session lager.Logger + + BeforeEach(func() { + session = logger.Session("sub-action") + }) + + Describe("the returned logger", func() { + JustBeforeEach(func() { + session.Debug("some-debug-action", lager.Data{"level": "debug"}) + session.Info("some-info-action", lager.Data{"level": "info"}) + session.Error("some-error-action", errors.New("oh no!"), lager.Data{"level": "error"}) + + defer func() { + recover() + }() + + session.Fatal("some-fatal-action", errors.New("oh no!"), lager.Data{"level": "fatal"}) + }) + + It("logs with a shared session id in the data", func() { + Expect(testSink.Logs()[0].Data["session"]).To(Equal("1")) + Expect(testSink.Logs()[1].Data["session"]).To(Equal("1")) + Expect(testSink.Logs()[2].Data["session"]).To(Equal("1")) + Expect(testSink.Logs()[3].Data["session"]).To(Equal("1")) + }) + + It("logs with the task added to the message", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.sub-action.some-debug-action")) + Expect(testSink.Logs()[1].Message).To(Equal("my-component.sub-action.some-info-action")) + Expect(testSink.Logs()[2].Message).To(Equal("my-component.sub-action.some-error-action")) + Expect(testSink.Logs()[3].Message).To(Equal("my-component.sub-action.some-fatal-action")) + }) + + It("logs with the original data", func() { + Expect(testSink.Logs()[0].Data["level"]).To(Equal("debug")) + Expect(testSink.Logs()[1].Data["level"]).To(Equal("info")) + Expect(testSink.Logs()[2].Data["level"]).To(Equal("error")) + Expect(testSink.Logs()[3].Data["level"]).To(Equal("fatal")) + }) + + Context("with data", func() { + BeforeEach(func() { + session = logger.Session("sub-action", lager.Data{"foo": "bar"}) + }) + + It("logs with the data added to the message", func() { + Expect(testSink.Logs()[0].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[1].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[2].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[3].Data["foo"]).To(Equal("bar")) + }) + + It("keeps the original data", func() { + Expect(testSink.Logs()[0].Data["level"]).To(Equal("debug")) + Expect(testSink.Logs()[1].Data["level"]).To(Equal("info")) + Expect(testSink.Logs()[2].Data["level"]).To(Equal("error")) + Expect(testSink.Logs()[3].Data["level"]).To(Equal("fatal")) + }) + }) + + Context("with another session", func() { + BeforeEach(func() { + session = logger.Session("next-sub-action") + }) + + It("logs with a shared session id in the data", func() { + Expect(testSink.Logs()[0].Data["session"]).To(Equal("2")) + Expect(testSink.Logs()[1].Data["session"]).To(Equal("2")) + Expect(testSink.Logs()[2].Data["session"]).To(Equal("2")) + Expect(testSink.Logs()[3].Data["session"]).To(Equal("2")) + }) + + It("logs with the task added to the message", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.next-sub-action.some-debug-action")) + Expect(testSink.Logs()[1].Message).To(Equal("my-component.next-sub-action.some-info-action")) + Expect(testSink.Logs()[2].Message).To(Equal("my-component.next-sub-action.some-error-action")) + Expect(testSink.Logs()[3].Message).To(Equal("my-component.next-sub-action.some-fatal-action")) + }) + }) + + Describe("WithData", func() { + BeforeEach(func() { + session = logger.WithData(lager.Data{"foo": "bar"}) + }) + + It("returns a new logger with the given data", func() { + Expect(testSink.Logs()[0].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[1].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[2].Data["foo"]).To(Equal("bar")) + Expect(testSink.Logs()[3].Data["foo"]).To(Equal("bar")) + }) + + It("does not append to the logger's task", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.some-debug-action")) + }) + }) + + Context("with a nested session", func() { + BeforeEach(func() { + session = session.Session("sub-sub-action") + }) + + It("logs with a shared session id in the data", func() { + Expect(testSink.Logs()[0].Data["session"]).To(Equal("1.1")) + Expect(testSink.Logs()[1].Data["session"]).To(Equal("1.1")) + Expect(testSink.Logs()[2].Data["session"]).To(Equal("1.1")) + Expect(testSink.Logs()[3].Data["session"]).To(Equal("1.1")) + }) + + It("logs with the task added to the message", func() { + Expect(testSink.Logs()[0].Message).To(Equal("my-component.sub-action.sub-sub-action.some-debug-action")) + Expect(testSink.Logs()[1].Message).To(Equal("my-component.sub-action.sub-sub-action.some-info-action")) + Expect(testSink.Logs()[2].Message).To(Equal("my-component.sub-action.sub-sub-action.some-error-action")) + Expect(testSink.Logs()[3].Message).To(Equal("my-component.sub-action.sub-sub-action.some-fatal-action")) + }) + }) + }) + }) + + Describe("Debug", func() { + Context("with log data", func() { + BeforeEach(func() { + logger.Debug(action, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.DEBUG) + TestLogData() + }) + + Context("with no log data", func() { + BeforeEach(func() { + logger.Debug(action) + }) + + TestCommonLogFeatures(lager.DEBUG) + }) + }) + + Describe("Info", func() { + Context("with log data", func() { + BeforeEach(func() { + logger.Info(action, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.INFO) + TestLogData() + }) + + Context("with no log data", func() { + BeforeEach(func() { + logger.Info(action) + }) + + TestCommonLogFeatures(lager.INFO) + }) + }) + + Describe("Error", func() { + var err = errors.New("oh noes!") + Context("with log data", func() { + BeforeEach(func() { + logger.Error(action, err, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.ERROR) + TestLogData() + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + }) + + Context("with no log data", func() { + BeforeEach(func() { + logger.Error(action, err) + }) + + TestCommonLogFeatures(lager.ERROR) + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + }) + + Context("with no error", func() { + BeforeEach(func() { + logger.Error(action, nil) + }) + + TestCommonLogFeatures(lager.ERROR) + + It("does not contain the error message", func() { + Expect(testSink.Logs()[0].Data).NotTo(HaveKey("error")) + }) + }) + }) + + Describe("Fatal", func() { + var err = errors.New("oh noes!") + var fatalErr interface{} + + Context("with log data", func() { + BeforeEach(func() { + defer func() { + fatalErr = recover() + }() + + logger.Fatal(action, err, logData, anotherLogData) + }) + + TestCommonLogFeatures(lager.FATAL) + TestLogData() + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + + It("data contains stack trace", func() { + Expect(testSink.Logs()[0].Data["trace"]).NotTo(BeEmpty()) + }) + + It("panics with the provided error", func() { + Expect(fatalErr).To(Equal(err)) + }) + }) + + Context("with no log data", func() { + BeforeEach(func() { + defer func() { + fatalErr = recover() + }() + + logger.Fatal(action, err) + }) + + TestCommonLogFeatures(lager.FATAL) + + It("data contains error message", func() { + Expect(testSink.Logs()[0].Data["error"]).To(Equal(err.Error())) + }) + + It("data contains stack trace", func() { + Expect(testSink.Logs()[0].Data["trace"]).NotTo(BeEmpty()) + }) + + It("panics with the provided error", func() { + Expect(fatalErr).To(Equal(err)) + }) + }) + + Context("with no error", func() { + BeforeEach(func() { + defer func() { + fatalErr = recover() + }() + + logger.Fatal(action, nil) + }) + + TestCommonLogFeatures(lager.FATAL) + + It("does not contain the error message", func() { + Expect(testSink.Logs()[0].Data).NotTo(HaveKey("error")) + }) + + It("data contains stack trace", func() { + Expect(testSink.Logs()[0].Data["trace"]).NotTo(BeEmpty()) + }) + + It("panics with the provided error (i.e. nil)", func() { + Expect(fatalErr).To(BeNil()) + }) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/models.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/models.go new file mode 100644 index 000000000000..94c0dac459ee --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/models.go @@ -0,0 +1,30 @@ +package lager + +import "encoding/json" + +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + ERROR + FATAL +) + +type Data map[string]interface{} + +type LogFormat struct { + Timestamp string `json:"timestamp"` + Source string `json:"source"` + Message string `json:"message"` + LogLevel LogLevel `json:"log_level"` + Data Data `json:"data"` +} + +func (log LogFormat) ToJSON() []byte { + content, err := json.Marshal(log) + if err != nil { + panic(err) + } + return content +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/reconfigurable_sink.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/reconfigurable_sink.go new file mode 100644 index 000000000000..7c3b228e3bef --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/reconfigurable_sink.go @@ -0,0 +1,35 @@ +package lager + +import "sync/atomic" + +type ReconfigurableSink struct { + sink Sink + + minLogLevel int32 +} + +func NewReconfigurableSink(sink Sink, initialMinLogLevel LogLevel) *ReconfigurableSink { + return &ReconfigurableSink{ + sink: sink, + + minLogLevel: int32(initialMinLogLevel), + } +} + +func (sink *ReconfigurableSink) Log(log LogFormat) { + minLogLevel := LogLevel(atomic.LoadInt32(&sink.minLogLevel)) + + if log.LogLevel < minLogLevel { + return + } + + sink.sink.Log(log) +} + +func (sink *ReconfigurableSink) SetMinLevel(level LogLevel) { + atomic.StoreInt32(&sink.minLogLevel, int32(level)) +} + +func (sink *ReconfigurableSink) GetMinLevel() LogLevel { + return LogLevel(atomic.LoadInt32(&sink.minLogLevel)) +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/reconfigurable_sink_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/reconfigurable_sink_test.go new file mode 100644 index 000000000000..466b73707d39 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/reconfigurable_sink_test.go @@ -0,0 +1,66 @@ +package lager_test + +import ( + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/lagertest" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ReconfigurableSink", func() { + var ( + testSink *lagertest.TestSink + + sink *lager.ReconfigurableSink + ) + + BeforeEach(func() { + testSink = lagertest.NewTestSink() + + sink = lager.NewReconfigurableSink(testSink, lager.INFO) + }) + + It("returns the current level", func() { + Expect(sink.GetMinLevel()).To(Equal(lager.INFO)) + }) + + Context("when logging above the minimum log level", func() { + var log lager.LogFormat + + BeforeEach(func() { + log = lager.LogFormat{LogLevel: lager.INFO, Message: "hello world"} + sink.Log(log) + }) + + It("writes to the given sink", func() { + Expect(testSink.Buffer().Contents()).To(MatchJSON(log.ToJSON())) + }) + }) + + Context("when logging below the minimum log level", func() { + BeforeEach(func() { + sink.Log(lager.LogFormat{LogLevel: lager.DEBUG, Message: "hello world"}) + }) + + It("does not write to the given writer", func() { + Expect(testSink.Buffer().Contents()).To(BeEmpty()) + }) + }) + + Context("when reconfigured to a new log level", func() { + BeforeEach(func() { + sink.SetMinLevel(lager.DEBUG) + }) + + It("writes logs above the new log level", func() { + log := lager.LogFormat{LogLevel: lager.DEBUG, Message: "hello world"} + sink.Log(log) + Expect(testSink.Buffer().Contents()).To(MatchJSON(log.ToJSON())) + }) + + It("returns the newly updated level", func() { + Expect(sink.GetMinLevel()).To(Equal(lager.DEBUG)) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/writer_sink.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/writer_sink.go new file mode 100644 index 000000000000..bb8fbf151037 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/writer_sink.go @@ -0,0 +1,38 @@ +package lager + +import ( + "io" + "sync" +) + +// A Sink represents a write destination for a Logger. It provides +// a thread-safe interface for writing logs +type Sink interface { + //Log to the sink. Best effort -- no need to worry about errors. + Log(LogFormat) +} + +type writerSink struct { + writer io.Writer + minLogLevel LogLevel + writeL *sync.Mutex +} + +func NewWriterSink(writer io.Writer, minLogLevel LogLevel) Sink { + return &writerSink{ + writer: writer, + minLogLevel: minLogLevel, + writeL: new(sync.Mutex), + } +} + +func (sink *writerSink) Log(log LogFormat) { + if log.LogLevel < sink.minLogLevel { + return + } + + sink.writeL.Lock() + sink.writer.Write(log.ToJSON()) + sink.writer.Write([]byte("\n")) + sink.writeL.Unlock() +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/writer_sink_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/writer_sink_test.go new file mode 100644 index 000000000000..ca9ba0b345b8 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/code.cloudfoundry.org/lager/writer_sink_test.go @@ -0,0 +1,107 @@ +package lager_test + +import ( + "fmt" + "runtime" + "strings" + "sync" + + "code.cloudfoundry.org/lager" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("WriterSink", func() { + const MaxThreads = 100 + + var sink lager.Sink + var writer *copyWriter + + BeforeSuite(func() { + runtime.GOMAXPROCS(MaxThreads) + }) + + BeforeEach(func() { + writer = NewCopyWriter() + sink = lager.NewWriterSink(writer, lager.INFO) + }) + + Context("when logging above the minimum log level", func() { + BeforeEach(func() { + sink.Log(lager.LogFormat{LogLevel: lager.INFO, Message: "hello world"}) + }) + + It("writes to the given writer", func() { + Expect(writer.Copy()).To(MatchJSON(`{"message":"hello world","log_level":1,"timestamp":"","source":"","data":null}`)) + }) + }) + + Context("when logging below the minimum log level", func() { + BeforeEach(func() { + sink.Log(lager.LogFormat{LogLevel: lager.DEBUG, Message: "hello world"}) + }) + + It("does not write to the given writer", func() { + Expect(writer.Copy()).To(Equal([]byte{})) + }) + }) + + Context("when logging from multiple threads", func() { + var content = "abcdefg " + + BeforeEach(func() { + wg := new(sync.WaitGroup) + for i := 0; i < MaxThreads; i++ { + wg.Add(1) + go func() { + sink.Log(lager.LogFormat{LogLevel: lager.INFO, Message: content}) + wg.Done() + }() + } + wg.Wait() + }) + + It("writes to the given writer", func() { + lines := strings.Split(string(writer.Copy()), "\n") + for _, line := range lines { + if line == "" { + continue + } + Expect(line).To(MatchJSON(fmt.Sprintf(`{"message":"%s","log_level":1,"timestamp":"","source":"","data":null}`, content))) + } + }) + }) +}) + +// copyWriter is an INTENTIONALLY UNSAFE writer. Use it to test code that +// should be handling thread safety. +type copyWriter struct { + contents []byte + lock *sync.RWMutex +} + +func NewCopyWriter() *copyWriter { + return ©Writer{ + contents: []byte{}, + lock: new(sync.RWMutex), + } +} + +// no, we really mean RLock on write. +func (writer *copyWriter) Write(p []byte) (n int, err error) { + writer.lock.RLock() + defer writer.lock.RUnlock() + + writer.contents = append(writer.contents, p...) + return len(p), nil +} + +func (writer *copyWriter) Copy() []byte { + writer.lock.Lock() + defer writer.lock.Unlock() + + contents := make([]byte, len(writer.contents)) + copy(contents, writer.contents) + return contents +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/README.md b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/README.md index cdab8784d11d..56c67134f897 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/README.md +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/README.md @@ -179,6 +179,7 @@ package main import ( "fmt" "net/http" + "strings" "github.com/gorilla/mux" ) @@ -190,15 +191,25 @@ func handler(w http.ResponseWriter, r *http.Request) { func main() { r := mux.NewRouter() r.HandleFunc("/", handler) - r.HandleFunc("/products", handler) - r.HandleFunc("/articles", handler) - r.HandleFunc("/articles/{id}", handler) + r.Methods("POST").HandleFunc("/products", handler) + r.Methods("GET").HandleFunc("/articles", handler) + r.Methods("GET", "PUT").HandleFunc("/articles/{id}", handler) r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { t, err := route.GetPathTemplate() if err != nil { return err } - fmt.Println(t) + // p will contain regular expression is compatible with regular expression in Perl, Python, and other languages. + // for instance the regular expression for path '/articles/{id}' will be '^/articles/(?P[^/]+)$' + p, err := route.GetPathRegexp() + if err != nil { + return err + } + m, err := route.GetMethods() + if err != nil { + return err + } + fmt.Println(strings.Join(m, ","), t, p) return nil }) http.Handle("/", r) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/mux_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/mux_test.go index 405aca6de94b..19ef5a8cceb0 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/mux_test.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/mux_test.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strings" "testing" ) @@ -31,10 +32,13 @@ type routeTest struct { route *Route // the route being tested request *http.Request // a request to test the route vars map[string]string // the expected vars of the match - host string // the expected host of the match - path string // the expected path of the match - pathTemplate string // the expected path template to match - hostTemplate string // the expected host template to match + scheme string // the expected scheme of the built URL + host string // the expected host of the built URL + path string // the expected path of the built URL + pathTemplate string // the expected path template of the route + hostTemplate string // the expected host template of the route + methods []string // the expected route methods + pathRegexp string // the expected path regexp shouldMatch bool // whether the request is expected to match the route at all shouldRedirect bool // whether the request should result in a redirect } @@ -195,46 +199,6 @@ func TestHost(t *testing.T) { hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, shouldMatch: true, }, - { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/a"), - vars: map[string]string{"category": "a"}, - host: "", - path: "/a", - pathTemplate: `/{category:a|b/c}`, - shouldMatch: true, - }, - { - title: "Path route with single pattern with pipe, match", - route: new(Route).Path("/{category:a|b/c}"), - request: newRequest("GET", "http://localhost/b/c"), - vars: map[string]string{"category": "b/c"}, - host: "", - path: "/b/c", - pathTemplate: `/{category:a|b/c}`, - shouldMatch: true, - }, - { - title: "Path route with multiple patterns with pipe, match", - route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), - request: newRequest("GET", "http://localhost/a/product_name/1"), - vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, - host: "", - path: "/a/product_name/1", - pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, - }, - { - title: "Path route with multiple patterns with pipe, match", - route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), - request: newRequest("GET", "http://localhost/b/c/product_name/1"), - vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, - host: "", - path: "/b/c/product_name/1", - pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, - shouldMatch: true, - }, } for _, test := range tests { testRoute(t, test) @@ -270,6 +234,7 @@ func TestPath(t *testing.T) { host: "", path: "/111", pathTemplate: `/111/`, + pathRegexp: `^/111/$`, shouldMatch: false, }, { @@ -290,6 +255,7 @@ func TestPath(t *testing.T) { host: "", path: "/", pathTemplate: `/`, + pathRegexp: `^/$`, shouldMatch: true, }, { @@ -333,6 +299,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/111/{v1:[0-9]{3}}/333`, + pathRegexp: `^/111/(?P[0-9]{3})/333$`, shouldMatch: false, }, { @@ -343,6 +310,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, + pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, shouldMatch: true, }, { @@ -353,6 +321,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, + pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, shouldMatch: false, }, { @@ -363,6 +332,7 @@ func TestPath(t *testing.T) { host: "", path: "/a/product_name/1", pathTemplate: `/{category:a|(?:b/c)}/{product}/{id:[0-9]+}`, + pathRegexp: `^/(?Pa|(?:b/c))/(?P[^/]+)/(?P[0-9]+)$`, shouldMatch: true, }, { @@ -373,6 +343,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/111/{v-1:[0-9]{3}}/333`, + pathRegexp: `^/111/(?P[0-9]{3})/333$`, shouldMatch: true, }, { @@ -383,6 +354,7 @@ func TestPath(t *testing.T) { host: "", path: "/111/222/333", pathTemplate: `/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}`, + pathRegexp: `^/(?P[0-9]{3})/(?P[0-9]{3})/(?P[0-9]{3})$`, shouldMatch: true, }, { @@ -393,6 +365,7 @@ func TestPath(t *testing.T) { host: "", path: "/a/product_name/1", pathTemplate: `/{product-category:a|(?:b/c)}/{product-name}/{product-id:[0-9]+}`, + pathRegexp: `^/(?Pa|(?:b/c))/(?P[^/]+)/(?P[0-9]+)$`, shouldMatch: true, }, { @@ -403,6 +376,7 @@ func TestPath(t *testing.T) { host: "", path: "/daily-2016-01-01", pathTemplate: `/{type:(?i:daily|mini|variety)}-{date:\d{4,4}-\d{2,2}-\d{2,2}}`, + pathRegexp: `^/(?P(?i:daily|mini|variety))-(?P\d{4,4}-\d{2,2}-\d{2,2})$`, shouldMatch: true, }, { @@ -413,6 +387,47 @@ func TestPath(t *testing.T) { host: "", path: "/111/222", pathTemplate: `/{v1:[0-9]*}{v2:[a-z]*}/{v3:[0-9]*}`, + pathRegexp: `^/(?P[0-9]*)(?P[a-z]*)/(?P[0-9]*)$`, + shouldMatch: true, + }, + { + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/a"), + vars: map[string]string{"category": "a"}, + host: "", + path: "/a", + pathTemplate: `/{category:a|b/c}`, + shouldMatch: true, + }, + { + title: "Path route with single pattern with pipe, match", + route: new(Route).Path("/{category:a|b/c}"), + request: newRequest("GET", "http://localhost/b/c"), + vars: map[string]string{"category": "b/c"}, + host: "", + path: "/b/c", + pathTemplate: `/{category:a|b/c}`, + shouldMatch: true, + }, + { + title: "Path route with multiple patterns with pipe, match", + route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), + request: newRequest("GET", "http://localhost/a/product_name/1"), + vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, + host: "", + path: "/a/product_name/1", + pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, + shouldMatch: true, + }, + { + title: "Path route with multiple patterns with pipe, match", + route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), + request: newRequest("GET", "http://localhost/b/c/product_name/1"), + vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, + host: "", + path: "/b/c/product_name/1", + pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, shouldMatch: true, }, } @@ -421,6 +436,7 @@ func TestPath(t *testing.T) { testRoute(t, test) testTemplate(t, test) testUseEscapedRoute(t, test) + testRegexp(t, test) } } @@ -502,15 +518,28 @@ func TestPathPrefix(t *testing.T) { } } -func TestHostPath(t *testing.T) { +func TestSchemeHostPath(t *testing.T) { tests := []routeTest{ { title: "Host and Path route, match", route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), vars: map[string]string{}, - host: "", - path: "", + scheme: "http", + host: "aaa.bbb.ccc", + path: "/111/222/333", + pathTemplate: `/111/222/333`, + hostTemplate: `aaa.bbb.ccc`, + shouldMatch: true, + }, + { + title: "Scheme, Host, and Path route, match", + route: new(Route).Schemes("https").Host("aaa.bbb.ccc").Path("/111/222/333"), + request: newRequest("GET", "https://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{}, + scheme: "https", + host: "aaa.bbb.ccc", + path: "/111/222/333", pathTemplate: `/111/222/333`, hostTemplate: `aaa.bbb.ccc`, shouldMatch: true, @@ -520,8 +549,9 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), vars: map[string]string{}, - host: "", - path: "", + scheme: "http", + host: "aaa.bbb.ccc", + path: "/111/222/333", pathTemplate: `/111/222/333`, hostTemplate: `aaa.bbb.ccc`, shouldMatch: false, @@ -531,6 +561,19 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), vars: map[string]string{"v1": "bbb", "v2": "222"}, + scheme: "http", + host: "aaa.bbb.ccc", + path: "/111/222/333", + pathTemplate: `/111/{v2:[0-9]{3}}/333`, + hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, + shouldMatch: true, + }, + { + title: "Scheme, Host, and Path route with host and path patterns, match", + route: new(Route).Schemes("ftp", "ssss").Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), + request: newRequest("GET", "ssss://aaa.bbb.ccc/111/222/333"), + vars: map[string]string{"v1": "bbb", "v2": "222"}, + scheme: "ftp", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/111/{v2:[0-9]{3}}/333`, @@ -542,6 +585,7 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), vars: map[string]string{"v1": "bbb", "v2": "222"}, + scheme: "http", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/111/{v2:[0-9]{3}}/333`, @@ -553,6 +597,7 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + scheme: "http", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, @@ -564,6 +609,7 @@ func TestHostPath(t *testing.T) { route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, + scheme: "http", host: "aaa.bbb.ccc", path: "/111/222/333", pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, @@ -635,7 +681,6 @@ func TestHeaders(t *testing.T) { testRoute(t, test) testTemplate(t, test) } - } func TestMethods(t *testing.T) { @@ -647,6 +692,7 @@ func TestMethods(t *testing.T) { vars: map[string]string{}, host: "", path: "", + methods: []string{"GET", "POST"}, shouldMatch: true, }, { @@ -656,6 +702,7 @@ func TestMethods(t *testing.T) { vars: map[string]string{}, host: "", path: "", + methods: []string{"GET", "POST"}, shouldMatch: true, }, { @@ -665,13 +712,25 @@ func TestMethods(t *testing.T) { vars: map[string]string{}, host: "", path: "", + methods: []string{"GET", "POST"}, shouldMatch: false, }, + { + title: "Route without methods", + route: new(Route), + request: newRequest("PUT", "http://localhost"), + vars: map[string]string{}, + host: "", + path: "", + methods: []string{}, + shouldMatch: true, + }, } for _, test := range tests { testRoute(t, test) testTemplate(t, test) + testMethods(t, test) } } @@ -910,30 +969,43 @@ func TestSchemes(t *testing.T) { tests := []routeTest{ // Schemes { - title: "Schemes route, match https", - route: new(Route).Schemes("https", "ftp"), + title: "Schemes route, default scheme, match http, build http", + route: new(Route).Host("localhost"), + request: newRequest("GET", "http://localhost"), + scheme: "http", + host: "localhost", + shouldMatch: true, + }, + { + title: "Schemes route, match https, build https", + route: new(Route).Schemes("https", "ftp").Host("localhost"), request: newRequest("GET", "https://localhost"), - vars: map[string]string{}, - host: "", - path: "", + scheme: "https", + host: "localhost", shouldMatch: true, }, { - title: "Schemes route, match ftp", - route: new(Route).Schemes("https", "ftp"), + title: "Schemes route, match ftp, build https", + route: new(Route).Schemes("https", "ftp").Host("localhost"), request: newRequest("GET", "ftp://localhost"), - vars: map[string]string{}, - host: "", - path: "", + scheme: "https", + host: "localhost", + shouldMatch: true, + }, + { + title: "Schemes route, match ftp, build ftp", + route: new(Route).Schemes("ftp", "https").Host("localhost"), + request: newRequest("GET", "ftp://localhost"), + scheme: "ftp", + host: "localhost", shouldMatch: true, }, { title: "Schemes route, bad scheme", - route: new(Route).Schemes("https", "ftp"), + route: new(Route).Schemes("https", "ftp").Host("localhost"), request: newRequest("GET", "http://localhost"), - vars: map[string]string{}, - host: "", - path: "", + scheme: "https", + host: "localhost", shouldMatch: false, }, } @@ -1393,7 +1465,7 @@ func TestSubrouterErrorHandling(t *testing.T) { func TestPanicOnCapturingGroups(t *testing.T) { defer func() { if recover() == nil { - t.Errorf("(Test that capturing groups now fail fast) Expected panic, however test completed sucessfully.\n") + t.Errorf("(Test that capturing groups now fail fast) Expected panic, however test completed successfully.\n") } }() NewRouter().NewRoute().Path("/{type:(promo|special)}/{promoId}.json") @@ -1420,10 +1492,15 @@ func testRoute(t *testing.T, test routeTest) { route := test.route vars := test.vars shouldMatch := test.shouldMatch - host := test.host - path := test.path - url := test.host + test.path shouldRedirect := test.shouldRedirect + uri := url.URL{ + Scheme: test.scheme, + Host: test.host, + Path: test.path, + } + if uri.Scheme == "" { + uri.Scheme = "http" + } var match RouteMatch ok := route.Match(request, &match) @@ -1436,28 +1513,51 @@ func testRoute(t *testing.T, test routeTest) { return } if shouldMatch { - if test.vars != nil && !stringMapEqual(test.vars, match.Vars) { + if vars != nil && !stringMapEqual(vars, match.Vars) { t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) return } - if host != "" { - u, _ := test.route.URLHost(mapToPairs(match.Vars)...) - if host != u.Host { - t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route)) + if test.scheme != "" { + u, err := route.URL(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if uri.Scheme != u.Scheme { + t.Errorf("(%v) URLScheme not equal: expected %v, got %v", test.title, uri.Scheme, u.Scheme) return } } - if path != "" { - u, _ := route.URLPath(mapToPairs(match.Vars)...) - if path != u.Path { - t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route)) + if test.host != "" { + u, err := test.route.URLHost(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URLHost error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if uri.Scheme != u.Scheme { + t.Errorf("(%v) URLHost scheme not equal: expected %v, got %v -- %v", test.title, uri.Scheme, u.Scheme, getRouteTemplate(route)) + return + } + if uri.Host != u.Host { + t.Errorf("(%v) URLHost host not equal: expected %v, got %v -- %v", test.title, uri.Host, u.Host, getRouteTemplate(route)) + return + } + } + if test.path != "" { + u, err := route.URLPath(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URLPath error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if uri.Path != u.Path { + t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, uri.Path, u.Path, getRouteTemplate(route)) return } } - if url != "" { - u, _ := route.URL(mapToPairs(match.Vars)...) - if url != u.Host+u.Path { - t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route)) + if test.host != "" && test.path != "" { + u, err := route.URL(mapToPairs(match.Vars)...) + if err != nil { + t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) + } + if expected, got := uri.String(), u.String(); expected != got { + t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, expected, got, getRouteTemplate(route)) return } } @@ -1499,6 +1599,22 @@ func testTemplate(t *testing.T, test routeTest) { } } +func testMethods(t *testing.T, test routeTest) { + route := test.route + methods, _ := route.GetMethods() + if strings.Join(methods, ",") != strings.Join(test.methods, ",") { + t.Errorf("(%v) GetMethods not equal: expected %v, got %v", test.title, test.methods, methods) + } +} + +func testRegexp(t *testing.T, test routeTest) { + route := test.route + routePathRegexp, regexpErr := route.GetPathRegexp() + if test.pathRegexp != "" && regexpErr == nil && routePathRegexp != test.pathRegexp { + t.Errorf("(%v) GetPathRegexp not equal: expected %v, got %v", test.title, test.pathRegexp, routePathRegexp) + } +} + type TestA301ResponseWriter struct { hh http.Header status int diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/route.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/route.go index 5544c1fd6ba4..56dcbbdc50ad 100644 --- a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/route.go +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/gorilla/mux/route.go @@ -31,6 +31,8 @@ type Route struct { skipClean bool // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" useEncodedPath bool + // The scheme used when building URLs. + buildScheme string // If true, this route never matches: it is only used to build URLs. buildOnly bool // The name used to build URLs. @@ -394,6 +396,9 @@ func (r *Route) Schemes(schemes ...string) *Route { for k, v := range schemes { schemes[k] = strings.ToLower(v) } + if r.buildScheme == "" && len(schemes) > 0 { + r.buildScheme = schemes[0] + } return r.addMatcher(schemeMatcher(schemes)) } @@ -478,11 +483,13 @@ func (r *Route) URL(pairs ...string) (*url.URL, error) { } var scheme, host, path string if r.regexp.host != nil { - // Set a default scheme. - scheme = "http" if host, err = r.regexp.host.url(values); err != nil { return nil, err } + scheme = "http" + if r.buildScheme != "" { + scheme = r.buildScheme + } } if r.regexp.path != nil { if path, err = r.regexp.path.url(values); err != nil { @@ -514,10 +521,14 @@ func (r *Route) URLHost(pairs ...string) (*url.URL, error) { if err != nil { return nil, err } - return &url.URL{ + u := &url.URL{ Scheme: "http", Host: host, - }, nil + } + if r.buildScheme != "" { + u.Scheme = r.buildScheme + } + return u, nil } // URLPath builds the path part of the URL for a route. See Route.URL(). @@ -558,6 +569,36 @@ func (r *Route) GetPathTemplate() (string, error) { return r.regexp.path.template, nil } +// GetPathRegexp returns the expanded regular expression used to match route path. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathRegexp() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.path == nil { + return "", errors.New("mux: route does not have a path") + } + return r.regexp.path.regexp.String(), nil +} + +// GetMethods returns the methods the route matches against +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An empty list will be returned if route does not have methods. +func (r *Route) GetMethods() ([]string, error) { + if r.err != nil { + return nil, r.err + } + for _, m := range r.matchers { + if methods, ok := m.(methodMatcher); ok { + return []string(methods), nil + } + } + return nil, nil +} + // GetHostTemplate returns the template used to build the // route match. // This is useful for building simple REST API documentation and for instrumentation diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/.travis.yml b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/.travis.yml new file mode 100644 index 000000000000..241fa1661fed --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/.travis.yml @@ -0,0 +1,11 @@ +language: go + +go: + - 1.7.3 + +install: + - go get -v github.com/tools/godep + - godep restore + - go get -v github.com/onsi/ginkgo/ginkgo + +script: ginkgo -r diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/Godeps/Godeps.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/Godeps/Godeps.json new file mode 100644 index 000000000000..27202aa3a338 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/Godeps/Godeps.json @@ -0,0 +1,180 @@ +{ + "ImportPath": "github.com/pivotal-cf/brokerapi", + "GoVersion": "go1.7", + "GodepVersion": "v74", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "code.cloudfoundry.org/lager", + "Rev": "62951a8009ab331bb21dc418074fa54e66eb9b6a" + }, + { + "ImportPath": "code.cloudfoundry.org/lager/lagertest", + "Rev": "62951a8009ab331bb21dc418074fa54e66eb9b6a" + }, + { + "ImportPath": "github.com/drewolson/testflight", + "Rev": "7040c250b4721006c536587ff8ee8ff573838edb" + }, + { + "ImportPath": "github.com/gorilla/mux", + "Comment": "v1.1-21-gcf79e51", + "Rev": "cf79e51a62d8219d52060dfc1b4e810414ba2d15" + }, + { + "ImportPath": "github.com/onsi/ginkgo", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/config", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/codelocation", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/containernode", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/failer", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/leafnodes", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/remote", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/spec", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/specrunner", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/suite", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/testingtproxy", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/internal/writer", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/reporters", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/reporters/stenographer", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/ginkgo/types", + "Comment": "v1.2.0-70-g43e2af1", + "Rev": "43e2af1f01ace55adbb6d7d0f30416476db1baae" + }, + { + "ImportPath": "github.com/onsi/gomega", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/format", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/gbytes", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/assertion", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/asyncassertion", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/oraclematcher", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/internal/testingtsupport", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/edge", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/node", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/util", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/onsi/gomega/types", + "Comment": "v1.0-119-gc90bd38", + "Rev": "c90bd38f8da6e92f8b114953db2f0ad7361fb4b4" + }, + { + "ImportPath": "github.com/pborman/uuid", + "Comment": "v1.0-11-gc55201b", + "Rev": "c55201b036063326c5b1b89ccfe45a184973d073" + }, + { + "ImportPath": "golang.org/x/sys/unix", + "Rev": "a408501be4d17ee978c04a618e7a1b22af058c0e" + }, + { + "ImportPath": "gopkg.in/yaml.v2", + "Rev": "e4d366fc3c7938e2958e662b4258c7a89e1f0e3e" + } + ] +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/Godeps/Readme b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/Godeps/Readme new file mode 100644 index 000000000000..4cdaa53d56d7 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/LICENSE b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/LICENSE new file mode 100644 index 000000000000..5c304d1a4a7b --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/NOTICE b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/NOTICE new file mode 100644 index 000000000000..85a3bd09f029 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/NOTICE @@ -0,0 +1,10 @@ +brokerapi + +Copyright (c) 2014-2015 Pivotal Software, Inc. All Rights Reserved. + +This product is licensed to you under the Apache License, Version 2.0 (the "License"). +You may not use this product except in compliance with the License. + +This product may include a number of subcomponents with separate copyright notices +and license terms. Your use of these subcomponents is subject to the terms and +conditions of the subcomponent's license, as noted in the LICENSE file. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/README.md b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/README.md new file mode 100644 index 000000000000..dc66bef30128 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/README.md @@ -0,0 +1,31 @@ +# brokerapi + +[![Build Status](https://travis-ci.org/pivotal-cf/brokerapi.svg?branch=master)](https://travis-ci.org/pivotal-cf/brokerapi) + +A Go package for building [V2 Cloud Foundry Service Brokers](https://docs.cloudfoundry.org/services/api.html). + +## [Docs](https://godoc.org/github.com/pivotal-cf/brokerapi) + +## Dependencies + +- Go 1.7+ +- [lager](https://github.com/cloudfoundry/lager) +- [gorilla/mux](https://github.com/gorilla/mux) + +## Usage + +`brokerapi` defines a [`ServiceBroker`](https://godoc.org/github.com/pivotal-cf/brokerapi#ServiceBroker) interface. Pass an implementation of this to [`brokerapi.New`](https://godoc.org/github.com/pivotal-cf/brokerapi#New), which returns an `http.Handler` that you can use to serve handle HTTP requests. + +Alternatively, if you already have a `*mux.Router` that you want to attach service broker routes to, you can use [`brokerapi.AttachRoutes`](https://godoc.org/github.com/pivotal-cf/brokerapi#AttachRoutes). + +## Error types + +`brokerapi` defines a handful of error types in `service_broker.go` for some common error cases that your service broker may encounter. Return these from your `ServiceBroker` methods where appropriate, and `brokerapi` will do the "right thing" (â„¢), and give Cloud Foundry an appropriate status code, as per the [Service Broker API specification](https://docs.cloudfoundry.org/services/api.html). + +### Custom Errors + +`NewFailureResponse()` allows you to return a custom error from any of the `ServiceBroker` interface methods which return an error. Within this you must define an error, a HTTP response status code and a logging key. You can also use the `NewFailureResponseBuilder()` to add a custom `Error:` value in the response, or indicate that the broker should return an empty response rather than the error message. + +## Example Service Broker + +You can see the [cf-redis](https://github.com/pivotal-cf/cf-redis-broker/blob/2f0e9a8ebb1012a9be74bbef2d411b0b3b60352f/broker/broker.go) service broker uses the BrokerAPI package to create a service broker for Redis. diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api.go new file mode 100644 index 000000000000..0f5655b87753 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api.go @@ -0,0 +1,355 @@ +package brokerapi + +import ( + "encoding/json" + "net/http" + "strconv" + + "code.cloudfoundry.org/lager" + "github.com/gorilla/mux" + "github.com/pivotal-cf/brokerapi/auth" +) + +const ( + provisionLogKey = "provision" + deprovisionLogKey = "deprovision" + bindLogKey = "bind" + unbindLogKey = "unbind" + lastOperationLogKey = "lastOperation" + + instanceIDLogKey = "instance-id" + instanceDetailsLogKey = "instance-details" + bindingIDLogKey = "binding-id" + + invalidServiceDetailsErrorKey = "invalid-service-details" + invalidBindDetailsErrorKey = "invalid-bind-details" + instanceLimitReachedErrorKey = "instance-limit-reached" + instanceAlreadyExistsErrorKey = "instance-already-exists" + bindingAlreadyExistsErrorKey = "binding-already-exists" + instanceMissingErrorKey = "instance-missing" + bindingMissingErrorKey = "binding-missing" + asyncRequiredKey = "async-required" + planChangeNotSupportedKey = "plan-change-not-supported" + unknownErrorKey = "unknown-error" + invalidRawParamsKey = "invalid-raw-params" + appGuidNotProvidedErrorKey = "app-guid-not-provided" +) + +type BrokerCredentials struct { + Username string + Password string +} + +func New(serviceBroker ServiceBroker, logger lager.Logger, brokerCredentials BrokerCredentials) http.Handler { + router := mux.NewRouter() + AttachRoutes(router, serviceBroker, logger) + return auth.NewWrapper(brokerCredentials.Username, brokerCredentials.Password).Wrap(router) +} + +func AttachRoutes(router *mux.Router, serviceBroker ServiceBroker, logger lager.Logger) { + handler := serviceBrokerHandler{serviceBroker: serviceBroker, logger: logger} + router.HandleFunc("/v2/catalog", handler.catalog).Methods("GET") + + router.HandleFunc("/v2/service_instances/{instance_id}", handler.provision).Methods("PUT") + router.HandleFunc("/v2/service_instances/{instance_id}", handler.deprovision).Methods("DELETE") + router.HandleFunc("/v2/service_instances/{instance_id}/last_operation", handler.lastOperation).Methods("GET") + router.HandleFunc("/v2/service_instances/{instance_id}", handler.update).Methods("PATCH") + + router.HandleFunc("/v2/service_instances/{instance_id}/service_bindings/{binding_id}", handler.bind).Methods("PUT") + router.HandleFunc("/v2/service_instances/{instance_id}/service_bindings/{binding_id}", handler.unbind).Methods("DELETE") +} + +type serviceBrokerHandler struct { + serviceBroker ServiceBroker + logger lager.Logger +} + +func (h serviceBrokerHandler) catalog(w http.ResponseWriter, req *http.Request) { + catalog := CatalogResponse{ + Services: h.serviceBroker.Services(req.Context()), + } + + h.respond(w, http.StatusOK, catalog) +} + +func (h serviceBrokerHandler) provision(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + + logger := h.logger.Session(provisionLogKey, lager.Data{ + instanceIDLogKey: instanceID, + }) + + var details ProvisionDetails + if err := json.NewDecoder(req.Body).Decode(&details); err != nil { + logger.Error(invalidServiceDetailsErrorKey, err) + h.respond(w, http.StatusUnprocessableEntity, ErrorResponse{ + Description: err.Error(), + }) + return + } + + acceptsIncompleteFlag, _ := strconv.ParseBool(req.URL.Query().Get("accepts_incomplete")) + + logger = logger.WithData(lager.Data{ + instanceDetailsLogKey: details, + }) + + provisionResponse, err := h.serviceBroker.Provision(req.Context(), instanceID, details, acceptsIncompleteFlag) + + if err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + if provisionResponse.IsAsync { + h.respond(w, http.StatusAccepted, ProvisioningResponse{ + DashboardURL: provisionResponse.DashboardURL, + OperationData: provisionResponse.OperationData, + }) + } else { + h.respond(w, http.StatusCreated, ProvisioningResponse{ + DashboardURL: provisionResponse.DashboardURL, + }) + } +} + +func (h serviceBrokerHandler) update(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + + var details UpdateDetails + if err := json.NewDecoder(req.Body).Decode(&details); err != nil { + h.logger.Error(invalidServiceDetailsErrorKey, err) + h.respond(w, http.StatusUnprocessableEntity, ErrorResponse{ + Description: err.Error(), + }) + return + } + + acceptsIncompleteFlag, _ := strconv.ParseBool(req.URL.Query().Get("accepts_incomplete")) + + updateServiceSpec, err := h.serviceBroker.Update(req.Context(), instanceID, details, acceptsIncompleteFlag) + if err != nil { + switch err := err.(type) { + case *FailureResponse: + h.logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(h.logger), err.ErrorResponse()) + default: + h.logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + statusCode := http.StatusOK + if updateServiceSpec.IsAsync { + statusCode = http.StatusAccepted + } + h.respond(w, statusCode, UpdateResponse{OperationData: updateServiceSpec.OperationData}) +} + +func (h serviceBrokerHandler) deprovision(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + logger := h.logger.Session(deprovisionLogKey, lager.Data{ + instanceIDLogKey: instanceID, + }) + + details := DeprovisionDetails{ + PlanID: req.FormValue("plan_id"), + ServiceID: req.FormValue("service_id"), + } + asyncAllowed := req.FormValue("accepts_incomplete") == "true" + + deprovisionSpec, err := h.serviceBroker.Deprovision(req.Context(), instanceID, details, asyncAllowed) + if err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + if deprovisionSpec.IsAsync { + h.respond(w, http.StatusAccepted, DeprovisionResponse{OperationData: deprovisionSpec.OperationData}) + } else { + h.respond(w, http.StatusOK, EmptyResponse{}) + } +} + +func (h serviceBrokerHandler) bind(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + bindingID := vars["binding_id"] + + logger := h.logger.Session(bindLogKey, lager.Data{ + instanceIDLogKey: instanceID, + bindingIDLogKey: bindingID, + }) + + var details BindDetails + if err := json.NewDecoder(req.Body).Decode(&details); err != nil { + logger.Error(invalidBindDetailsErrorKey, err) + h.respond(w, http.StatusUnprocessableEntity, ErrorResponse{ + Description: err.Error(), + }) + return + } + + binding, err := h.serviceBroker.Bind(req.Context(), instanceID, bindingID, details) + if err != nil { + switch err := err.(type) { + case *FailureResponse: + statusCode := err.ValidatedStatusCode(logger) + errorResponse := err.ErrorResponse() + if err == ErrInstanceDoesNotExist { + // work around ErrInstanceDoesNotExist having different pre-refactor behaviour to other actions + errorResponse = ErrorResponse{ + Description: err.Error(), + } + statusCode = http.StatusNotFound + } + logger.Error(err.LoggerAction(), err) + h.respond(w, statusCode, errorResponse) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + brokerAPIVersion := req.Header.Get("X-Broker-Api-Version") + if brokerAPIVersion == "2.8" || brokerAPIVersion == "2.9" { + experimentalVols := []ExperimentalVolumeMount{} + + for _, vol := range binding.VolumeMounts { + experimentalConfig, err := json.Marshal(vol.Device.MountConfig) + if err != nil { + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{Description: err.Error()}) + return + } + + experimentalVols = append(experimentalVols, ExperimentalVolumeMount{ + ContainerPath: vol.ContainerDir, + Mode: vol.Mode, + Private: ExperimentalVolumeMountPrivate{ + Driver: vol.Driver, + GroupID: vol.Device.VolumeId, + Config: string(experimentalConfig), + }, + }) + } + + experimentalBinding := ExperimentalVolumeMountBindingResponse{ + Credentials: binding.Credentials, + RouteServiceURL: binding.RouteServiceURL, + SyslogDrainURL: binding.SyslogDrainURL, + VolumeMounts: experimentalVols, + } + h.respond(w, http.StatusCreated, experimentalBinding) + return + } + + h.respond(w, http.StatusCreated, binding) +} + +func (h serviceBrokerHandler) unbind(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + bindingID := vars["binding_id"] + + logger := h.logger.Session(unbindLogKey, lager.Data{ + instanceIDLogKey: instanceID, + bindingIDLogKey: bindingID, + }) + + details := UnbindDetails{ + PlanID: req.FormValue("plan_id"), + ServiceID: req.FormValue("service_id"), + } + + if err := h.serviceBroker.Unbind(req.Context(), instanceID, bindingID, details); err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + h.respond(w, http.StatusOK, EmptyResponse{}) +} + +func (h serviceBrokerHandler) lastOperation(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + instanceID := vars["instance_id"] + operationData := req.FormValue("operation") + + logger := h.logger.Session(lastOperationLogKey, lager.Data{ + instanceIDLogKey: instanceID, + }) + + logger.Info("starting-check-for-operation") + + lastOperation, err := h.serviceBroker.LastOperation(req.Context(), instanceID, operationData) + + if err != nil { + switch err := err.(type) { + case *FailureResponse: + logger.Error(err.LoggerAction(), err) + h.respond(w, err.ValidatedStatusCode(logger), err.ErrorResponse()) + default: + logger.Error(unknownErrorKey, err) + h.respond(w, http.StatusInternalServerError, ErrorResponse{ + Description: err.Error(), + }) + } + return + } + + logger.WithData(lager.Data{"state": lastOperation.State}).Info("done-check-for-operation") + + lastOperationResponse := LastOperationResponse{ + State: lastOperation.State, + Description: lastOperation.Description, + } + + h.respond(w, http.StatusOK, lastOperationResponse) +} + +func (h serviceBrokerHandler) respond(w http.ResponseWriter, status int, response interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + encoder := json.NewEncoder(w) + err := encoder.Encode(response) + if err != nil { + h.logger.Error("encoding response", err, lager.Data{"status": status, "response": response}) + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api_suite_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api_suite_test.go new file mode 100644 index 000000000000..ecf111d8bea1 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api_suite_test.go @@ -0,0 +1,39 @@ +package brokerapi_test + +import ( + "fmt" + "io/ioutil" + "path" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pborman/uuid" +) + +func TestAPI(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "API Suite") +} + +func fixture(name string) string { + filePath := path.Join("fixtures", name) + contents, err := ioutil.ReadFile(filePath) + if err != nil { + panic(fmt.Sprintf("Could not read fixture: %s", name)) + } + + return string(contents) +} + +func uniqueID() string { + return uuid.NewRandom().String() +} + +func uniqueInstanceID() string { + return uniqueID() +} + +func uniqueBindingID() string { + return uniqueID() +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api_test.go new file mode 100644 index 000000000000..75558d429151 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/api_test.go @@ -0,0 +1,1541 @@ +package brokerapi_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + + "code.cloudfoundry.org/lager" + "code.cloudfoundry.org/lager/lagertest" + "github.com/drewolson/testflight" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pivotal-cf/brokerapi" + "github.com/pivotal-cf/brokerapi/fakes" +) + +var _ = Describe("Service Broker API", func() { + var fakeServiceBroker *fakes.FakeServiceBroker + var brokerAPI http.Handler + var brokerLogger *lagertest.TestLogger + var credentials = brokerapi.BrokerCredentials{ + Username: "username", + Password: "password", + } + + makeInstanceProvisioningRequest := func(instanceID string, details map[string]interface{}, queryString string) *testflight.Response { + response := &testflight.Response{} + + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := "/v2/service_instances/" + instanceID + queryString + + buffer := &bytes.Buffer{} + json.NewEncoder(buffer).Encode(details) + request, err := http.NewRequest("PUT", path, buffer) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth(credentials.Username, credentials.Password) + + response = r.Do(request) + }) + return response + } + + makeInstanceProvisioningRequestWithAcceptsIncomplete := func(instanceID string, details map[string]interface{}, acceptsIncomplete bool) *testflight.Response { + var acceptsIncompleteFlag string + + if acceptsIncomplete { + acceptsIncompleteFlag = "?accepts_incomplete=true" + } else { + acceptsIncompleteFlag = "?accepts_incomplete=false" + } + + return makeInstanceProvisioningRequest(instanceID, details, acceptsIncompleteFlag) + } + + lastLogLine := func() lager.LogFormat { + noOfLogLines := len(brokerLogger.Logs()) + if noOfLogLines == 0 { + // better way to raise error? + err := errors.New("expected some log lines but there were none") + Expect(err).NotTo(HaveOccurred()) + } + + return brokerLogger.Logs()[noOfLogLines-1] + } + + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + brokerLogger = lagertest.NewTestLogger("broker-api") + brokerAPI = brokerapi.New(fakeServiceBroker, brokerLogger, credentials) + }) + + Describe("response headers", func() { + makeRequest := func() *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + request.SetBasicAuth(credentials.Username, credentials.Password) + brokerAPI.ServeHTTP(recorder, request) + return recorder + } + + It("has a Content-Type header", func() { + response := makeRequest() + + header := response.Header().Get("Content-Type") + Ω(header).Should(Equal("application/json")) + }) + }) + + Describe("request context", func() { + var ( + ctx context.Context + ) + + makeRequest := func(method, path, body string) *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + request, _ := http.NewRequest(method, path, strings.NewReader(body)) + request.SetBasicAuth(credentials.Username, credentials.Password) + request = request.WithContext(ctx) + brokerAPI.ServeHTTP(recorder, request) + return recorder + } + + BeforeEach(func() { + ctx = context.WithValue(context.Background(), "test_context", true) + }) + + It("catalog endpoint passes the request context to the broker", func() { + makeRequest("GET", "/v2/catalog", "") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("provision endpoint passes the request context to the broker", func() { + makeRequest("PUT", "/v2/service_instances/instance-id", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("deprovision endpoint passes the request context to the broker", func() { + makeRequest("DELETE", "/v2/service_instances/instance-id", "") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("bind endpoint passes the request context to the broker", func() { + makeRequest("PUT", "/v2/service_instances/instance-id/service_bindings/binding-id", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("unbind endpoint passes the request context to the broker", func() { + makeRequest("DELETE", "/v2/service_instances/instance-id/service_bindings/binding-id", "") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("update endpoint passes the request context to the broker", func() { + makeRequest("PATCH", "/v2/service_instances/instance-id", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + + It("last operation endpoint passes the request context to the broker", func() { + makeRequest("GET", "/v2/service_instances/instance-id/last_operation", "{}") + Expect(fakeServiceBroker.ReceivedContext).To(BeTrue()) + }) + }) + + Describe("authentication", func() { + makeRequestWithoutAuth := func() *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + response = r.Do(request) + }) + return response + } + + makeRequestWithAuth := func(username string, password string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + request.SetBasicAuth(username, password) + + response = r.Do(request) + }) + return response + } + + makeRequestWithUnrecognizedAuth := func() *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + // dXNlcm5hbWU6cGFzc3dvcmQ= is base64 encoding of 'username:password', + // ie, a correctly encoded basic authorization header + request.Header["Authorization"] = []string{"NOTBASIC dXNlcm5hbWU6cGFzc3dvcmQ="} + + response = r.Do(request) + }) + return response + } + + It("returns 401 when the authorization header has an incorrect password", func() { + response := makeRequestWithAuth("username", "fake_password") + Expect(response.StatusCode).To(Equal(401)) + }) + + It("returns 401 when the authorization header has an incorrect username", func() { + response := makeRequestWithAuth("fake_username", "password") + Expect(response.StatusCode).To(Equal(401)) + }) + + It("returns 401 when there is no authorization header", func() { + response := makeRequestWithoutAuth() + Expect(response.StatusCode).To(Equal(401)) + }) + + It("returns 401 when there is a unrecognized authorization header", func() { + response := makeRequestWithUnrecognizedAuth() + Expect(response.StatusCode).To(Equal(401)) + }) + + It("does not call through to the service broker when not authenticated", func() { + makeRequestWithAuth("username", "fake_password") + Ω(fakeServiceBroker.BrokerCalled).ShouldNot(BeTrue(), + "broker should not have been hit when authentication failed", + ) + }) + }) + + Describe("catalog endpoint", func() { + makeCatalogRequest := func() *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + request, _ := http.NewRequest("GET", "/v2/catalog", nil) + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + It("returns a 200", func() { + response := makeCatalogRequest() + Expect(response.StatusCode).To(Equal(200)) + }) + + It("returns valid catalog json", func() { + response := makeCatalogRequest() + Expect(response.Body).To(MatchJSON(fixture("catalog.json"))) + }) + }) + + Describe("instance lifecycle endpoint", func() { + makeInstanceDeprovisioningRequest := func(instanceID, queryString string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s?plan_id=plan-id&service_id=service-id", instanceID) + if queryString != "" { + path = fmt.Sprintf("%s&%s", path, queryString) + } + request, err := http.NewRequest("DELETE", path, strings.NewReader("")) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth("username", "password") + + response = r.Do(request) + + }) + return response + } + + Describe("provisioning", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + provisionDetails = map[string]interface{}{ + "service_id": "service-id", + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + }) + + It("calls Provision on the service broker with all params", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(fakeServiceBroker.ProvisionDetails).To(Equal(brokerapi.ProvisionDetails{ + ServiceID: "service-id", + PlanID: "plan-id", + OrganizationGUID: "organization-guid", + SpaceGUID: "space-guid", + })) + }) + + It("calls Provision on the service broker with the instance id", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(fakeServiceBroker.ProvisionedInstanceIDs).To(ContainElement(instanceID)) + }) + + Context("when the broker returns some operation data", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + OperationDataToReturn: "some-operation-data", + } + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + ShouldProvisionAsync: true, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns the operation data to the cloud controller", func() { + resp := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(resp.Body).To(MatchJSON(fixture("operation_data_response.json"))) + }) + }) + + Context("when there are arbitrary params", func() { + var rawParams string + + BeforeEach(func() { + provisionDetails["parameters"] = map[string]interface{}{ + "string": "some-string", + "number": 1, + "object": struct{ Name string }{"some-name"}, + "array": []interface{}{"a", "b", "c"}, + } + rawParams = `{ + "string":"some-string", + "number":1, + "object": { "Name": "some-name" }, + "array": [ "a", "b", "c" ] + }` + }) + + It("calls Provision on the service broker with all params", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(string(fakeServiceBroker.ProvisionDetails.RawParameters)).To(MatchJSON(rawParams)) + }) + + It("calls Provision with details with raw parameters", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + detailsWithRawParameters := brokerapi.DetailsWithRawParameters(fakeServiceBroker.ProvisionDetails) + rawParameters := detailsWithRawParameters.GetRawParameters() + Expect(string(rawParameters)).To(MatchJSON(rawParams)) + }) + }) + + Context("when the instance does not exist", func() { + It("returns a 201", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(201)) + }) + + It("returns empty json", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(fixture("provisioning.json"))) + }) + + Context("when the broker returns a dashboard URL", func() { + BeforeEach(func() { + fakeServiceBroker.DashboardURL = "some-dashboard-url" + }) + + It("returns json with dasboard URL", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(fixture("provisioning_with_dashboard.json"))) + }) + }) + + Context("when the instance limit has been reached", func() { + BeforeEach(func() { + for i := 0; i < fakeServiceBroker.InstanceLimit; i++ { + makeInstanceProvisioningRequest(uniqueInstanceID(), provisionDetails, "") + } + }) + + It("returns a 500", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(fixture("instance_limit_error.json"))) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + + Expect(lastLogLine().Message).To(ContainSubstring(".provision.instance-limit-reached")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance limit for this service has been reached")) + }) + }) + + Context("when an unexpected error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.ProvisionError = errors.New("broker failed") + }) + + It("returns a 500", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{"description":"broker failed"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("broker failed")) + }) + }) + + Context("when a custom error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.ProvisionError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + + Context("RawParameters are not valid JSON", func() { + BeforeEach(func() { + fakeServiceBroker.ProvisionError = brokerapi.ErrRawParamsInvalid + }) + + It("returns a 422", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{"description":"The format of the parameters is not valid JSON"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.invalid-raw-params")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("The format of the parameters is not valid JSON")) + }) + }) + + Context("when we send invalid json", func() { + makeBadInstanceProvisioningRequest := func(instanceID string) *testflight.Response { + response := &testflight.Response{} + + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := "/v2/service_instances/" + instanceID + + body := strings.NewReader("{{{{{") + request, err := http.NewRequest("PUT", path, body) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth(credentials.Username, credentials.Password) + + response = r.Do(request) + }) + + return response + } + + It("returns a 422 bad request", func() { + response := makeBadInstanceProvisioningRequest(instanceID) + Expect(response.StatusCode).Should(Equal(http.StatusUnprocessableEntity)) + }) + + It("logs a message", func() { + makeBadInstanceProvisioningRequest(instanceID) + Expect(lastLogLine().Message).To(ContainSubstring(".provision.invalid-service-details")) + }) + }) + }) + + Context("when the instance already exists", func() { + BeforeEach(func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + It("returns a 409", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(409)) + }) + + It("returns an empty JSON object", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(lastLogLine().Message).To(ContainSubstring(".provision.instance-already-exists")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance already exists")) + }) + }) + + Describe("accepts_incomplete", func() { + Context("when the accepts_incomplete flag is true", func() { + It("calls ProvisionAsync on the service broker", func() { + acceptsIncomplete := true + makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, acceptsIncomplete) + Expect(fakeServiceBroker.ProvisionDetails).To(Equal(brokerapi.ProvisionDetails{ + ServiceID: "service-id", + PlanID: "plan-id", + OrganizationGUID: "organization-guid", + SpaceGUID: "space-guid", + })) + + Expect(fakeServiceBroker.ProvisionedInstanceIDs).To(ContainElement(instanceID)) + }) + + Context("when the broker chooses to provision asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + ShouldProvisionAsync: true, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 202", func() { + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, true) + Expect(response.StatusCode).To(Equal(http.StatusAccepted)) + }) + }) + + Context("when the broker chooses to provision synchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + ShouldProvisionAsync: false, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 201", func() { + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, true) + Expect(response.StatusCode).To(Equal(http.StatusCreated)) + }) + }) + }) + + Context("when the accepts_incomplete flag is false", func() { + It("returns a 201", func() { + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, false) + Expect(response.StatusCode).To(Equal(http.StatusCreated)) + }) + + Context("when broker can only respond asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 422", func() { + acceptsIncomplete := false + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, acceptsIncomplete) + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + Expect(response.Body).To(MatchJSON(fixture("async_required.json"))) + }) + }) + }) + + Context("when the accepts_incomplete flag is missing", func() { + It("returns a 201", func() { + response := makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + Expect(response.StatusCode).To(Equal(http.StatusCreated)) + }) + + Context("when broker can only respond asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker = &fakes.FakeServiceBroker{ + InstanceLimit: 3, + } + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + It("returns a 422", func() { + acceptsIncomplete := false + response := makeInstanceProvisioningRequestWithAcceptsIncomplete(instanceID, provisionDetails, acceptsIncomplete) + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + Expect(response.Body).To(MatchJSON(fixture("async_required.json"))) + }) + }) + }) + }) + }) + + Describe("updating", func() { + var ( + instanceID string + details map[string]interface{} + queryString string + + response *testflight.Response + ) + + makeInstanceUpdateRequest := func(instanceID string, details map[string]interface{}, queryString string) *testflight.Response { + response := &testflight.Response{} + + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := "/v2/service_instances/" + instanceID + queryString + + buffer := &bytes.Buffer{} + json.NewEncoder(buffer).Encode(details) + request, err := http.NewRequest("PATCH", path, buffer) + Expect(err).NotTo(HaveOccurred()) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth(credentials.Username, credentials.Password) + + response = r.Do(request) + }) + return response + } + + BeforeEach(func() { + instanceID = uniqueInstanceID() + details = map[string]interface{}{ + "service_id": "some-service-id", + "plan_id": "new-plan", + "parameters": map[string]interface{}{ + "new-param": "new-param-value", + }, + "previous_values": map[string]interface{}{ + "service_id": "service-id", + "plan_id": "old-plan", + "organization_id": "org-id", + "space_id": "space-id", + }, + } + }) + + JustBeforeEach(func() { + response = makeInstanceUpdateRequest(instanceID, details, queryString) + }) + + Context("when the broker returns no error", func() { + Context("when the broker responds synchronously", func() { + It("returns HTTP 200", func() { + Expect(response.StatusCode).To(Equal(http.StatusOK)) + }) + + It("returns JSON content type", func() { + Expect(response.RawResponse.Header.Get("Content-Type")).To(Equal("application/json")) + }) + + It("returns empty JSON body", func() { + Expect(response.Body).To(Equal("{}\n")) + }) + + It("calls broker with instanceID and update details", func() { + Expect(fakeServiceBroker.UpdatedInstanceIDs).To(ConsistOf(instanceID)) + Expect(fakeServiceBroker.UpdateDetails.ServiceID).To(Equal("some-service-id")) + Expect(fakeServiceBroker.UpdateDetails.PlanID).To(Equal("new-plan")) + Expect(fakeServiceBroker.UpdateDetails.PreviousValues).To(Equal(brokerapi.PreviousValues{ + PlanID: "old-plan", + ServiceID: "service-id", + OrgID: "org-id", + SpaceID: "space-id", + }, + )) + Expect(fakeServiceBroker.UpdateDetails.RawParameters).To(Equal(json.RawMessage(`{"new-param":"new-param-value"}`))) + }) + + It("calls update with details with raw parameters", func() { + detailsWithRawParameters := brokerapi.DetailsWithRawParameters(fakeServiceBroker.UpdateDetails) + rawParameters := detailsWithRawParameters.GetRawParameters() + Expect(rawParameters).To(Equal(json.RawMessage(`{"new-param":"new-param-value"}`))) + }) + + Context("when accepts_incomplete=true", func() { + BeforeEach(func() { + queryString = "?accepts_incomplete=true" + }) + + It("tells broker async is allowed", func() { + Expect(fakeServiceBroker.AsyncAllowed).To(BeTrue()) + }) + }) + + Context("when accepts_incomplete is not supplied", func() { + BeforeEach(func() { + queryString = "" + }) + + It("tells broker async not allowed", func() { + Expect(fakeServiceBroker.AsyncAllowed).To(BeFalse()) + }) + }) + }) + + Context("when the broker responds asynchronously", func() { + BeforeEach(func() { + fakeServiceBroker.ShouldReturnAsync = true + }) + + It("returns HTTP 202", func() { + Expect(response.StatusCode).To(Equal(http.StatusAccepted)) + }) + + Context("when the broker responds with operation data", func() { + BeforeEach(func() { + fakeServiceBroker.OperationDataToReturn = "some-operation-data" + }) + + It("returns the operation data to the cloud controller", func() { + Expect(response.Body).To(MatchJSON(fixture("operation_data_response.json"))) + }) + }) + }) + }) + + Context("when the broker indicates that it needs async support", func() { + BeforeEach(func() { + fakeServiceBroker.UpdateError = brokerapi.ErrAsyncRequired + }) + + It("returns HTTP 422", func() { + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + + It("returns a descriptive message", func() { + var body map[string]string + err := json.Unmarshal([]byte(response.Body), &body) + Expect(err).ToNot(HaveOccurred()) + Expect(body["error"]).To(Equal("AsyncRequired")) + Expect(body["description"]).To(Equal("This service plan requires client support for asynchronous service operations.")) + }) + }) + + Context("when the broker indicates that the plan cannot be upgraded", func() { + BeforeEach(func() { + fakeServiceBroker.UpdateError = brokerapi.ErrPlanChangeNotSupported + }) + + It("returns HTTP 422", func() { + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + + It("returns a descriptive message", func() { + var body map[string]string + err := json.Unmarshal([]byte(response.Body), &body) + Expect(err).ToNot(HaveOccurred()) + Expect(body["error"]).To(Equal("PlanChangeNotSupported")) + Expect(body["description"]).To(Equal("The requested plan migration cannot be performed")) + }) + }) + + Context("when the broker errors in an unknown way", func() { + BeforeEach(func() { + fakeServiceBroker.UpdateError = errors.New("some horrible internal error") + }) + + It("returns HTTP 500", func() { + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns a descriptive message", func() { + var body map[string]string + err := json.Unmarshal([]byte(response.Body), &body) + Expect(err).ToNot(HaveOccurred()) + Expect(body["description"]).To(Equal("some horrible internal error")) + }) + }) + }) + + Describe("deprovisioning", func() { + It("calls Deprovision on the service broker with the instance id", func() { + instanceID := uniqueInstanceID() + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(fakeServiceBroker.DeprovisionedInstanceIDs).To(ContainElement(instanceID)) + }) + + Context("when the instance exists", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + + provisionDetails = map[string]interface{}{ + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + itReturnsStatus := func(expectedStatus int, queryString string) { + It(fmt.Sprintf("returns HTTP %d", expectedStatus), func() { + response := makeInstanceDeprovisioningRequest(instanceID, queryString) + Expect(response.StatusCode).To(Equal(expectedStatus)) + }) + } + + itReturnsEmptyJsonObject := func(queryString string) { + It("returns an empty JSON object", func() { + response := makeInstanceDeprovisioningRequest(instanceID, queryString) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + } + + Context("when the broker can only operate synchronously", func() { + Context("when the accepts_incomplete flag is not set", func() { + itReturnsStatus(200, "") + itReturnsEmptyJsonObject("") + }) + + Context("when the accepts_incomplete flag is set to true", func() { + itReturnsStatus(200, "accepts_incomplete=true") + itReturnsEmptyJsonObject("accepts_incomplete=true") + }) + }) + + Context("when the broker can only operate asynchronously", func() { + BeforeEach(func() { + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + Context("when the accepts_incomplete flag is not set", func() { + itReturnsStatus(http.StatusUnprocessableEntity, "") + + It("returns a descriptive error", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.Body).To(MatchJSON(fixture("async_required.json"))) + }) + }) + + Context("when the accepts_incomplete flag is set to true", func() { + itReturnsStatus(202, "accepts_incomplete=true") + itReturnsEmptyJsonObject("accepts_incomplete=true") + }) + + Context("when the broker returns operation data", func() { + BeforeEach(func() { + fakeServiceBroker.OperationDataToReturn = "some-operation-data" + fakeAsyncServiceBroker := &fakes.FakeAsyncOnlyServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + itReturnsStatus(202, "accepts_incomplete=true") + + It("returns the operation data to the cloud controller", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "accepts_incomplete=true") + Expect(response.Body).To(MatchJSON(fixture("operation_data_response.json"))) + }) + }) + }) + + Context("when the broker can operate both synchronously and asynchronously", func() { + BeforeEach(func() { + fakeAsyncServiceBroker := &fakes.FakeAsyncServiceBroker{ + FakeServiceBroker: *fakeServiceBroker, + } + brokerAPI = brokerapi.New(fakeAsyncServiceBroker, brokerLogger, credentials) + }) + + Context("when the accepts_incomplete flag is not set", func() { + itReturnsStatus(200, "") + itReturnsEmptyJsonObject("") + }) + + Context("when the accepts_incomplete flag is set to true", func() { + itReturnsStatus(202, "accepts_incomplete=true") + itReturnsEmptyJsonObject("accepts_incomplete=true") + }) + }) + + It("contains plan_id", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(fakeServiceBroker.DeprovisionDetails.PlanID).To(Equal("plan-id")) + }) + + It("contains service_id", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(fakeServiceBroker.DeprovisionDetails.ServiceID).To(Equal("service-id")) + }) + }) + + Context("when the instance does not exist", func() { + var instanceID string + + It("returns a 410", func() { + response := makeInstanceDeprovisioningRequest(uniqueInstanceID(), "") + Expect(response.StatusCode).To(Equal(410)) + }) + + It("returns an empty JSON object", func() { + response := makeInstanceDeprovisioningRequest(uniqueInstanceID(), "") + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(lastLogLine().Message).To(ContainSubstring(".deprovision.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + }) + }) + + Context("when instance deprovisioning fails", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + provisionDetails = map[string]interface{}{ + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + Context("when an unexpected error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.DeprovisionError = errors.New("broker failed") + }) + + It("returns a 500", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.StatusCode).To(Equal(500)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.Body).To(MatchJSON(`{"description":"broker failed"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(lastLogLine().Message).To(ContainSubstring(".deprovision.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("broker failed")) + }) + }) + + Context("when a custom error occurs", func() { + BeforeEach(func() { + fakeServiceBroker.DeprovisionError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeInstanceDeprovisioningRequest(instanceID, "") + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeInstanceDeprovisioningRequest(instanceID, "") + Expect(lastLogLine().Message).To(ContainSubstring(".deprovision.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + }) + }) + + Describe("binding lifecycle endpoint", func() { + makeBindingRequestWithSpecificAPIVersion := func(instanceID, bindingID string, details map[string]interface{}, apiVersion string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s/service_bindings/%s", + instanceID, bindingID) + + buffer := &bytes.Buffer{} + + if details != nil { + json.NewEncoder(buffer).Encode(details) + } + + request, err := http.NewRequest("PUT", path, buffer) + + Expect(err).NotTo(HaveOccurred()) + + request.Header.Add("Content-Type", "application/json") + request.Header.Add("X-Broker-Api-Version", apiVersion) + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + makeBindingRequest := func(instanceID, bindingID string, details map[string]interface{}) *testflight.Response { + return makeBindingRequestWithSpecificAPIVersion(instanceID, bindingID, details, "2.10") + } + + Describe("binding", func() { + var ( + instanceID string + bindingID string + details map[string]interface{} + ) + + BeforeEach(func() { + instanceID = uniqueInstanceID() + bindingID = uniqueBindingID() + details = map[string]interface{}{ + "app_guid": "app_guid", + "plan_id": "plan_id", + "service_id": "service_id", + "parameters": map[string]interface{}{ + "new-param": "new-param-value", + }, + } + }) + + Context("when the associated instance exists", func() { + It("calls Bind on the service broker with the instance and binding ids", func() { + makeBindingRequest(instanceID, bindingID, details) + Expect(fakeServiceBroker.BoundInstanceIDs).To(ContainElement(instanceID)) + Expect(fakeServiceBroker.BoundBindingIDs).To(ContainElement(bindingID)) + Expect(fakeServiceBroker.BoundBindingDetails).To(Equal(brokerapi.BindDetails{ + AppGUID: "app_guid", + PlanID: "plan_id", + ServiceID: "service_id", + RawParameters: json.RawMessage(`{"new-param":"new-param-value"}`), + })) + }) + + It("calls bind with details with raw parameters", func() { + makeBindingRequest(instanceID, bindingID, details) + detailsWithRawParameters := brokerapi.DetailsWithRawParameters(fakeServiceBroker.BoundBindingDetails) + rawParameters := detailsWithRawParameters.GetRawParameters() + Expect(rawParameters).To(Equal(json.RawMessage(`{"new-param":"new-param-value"}`))) + }) + + It("returns the credentials returned by Bind", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding.json"))) + }) + + It("returns a 201", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(201)) + }) + + Context("when syslog_drain_url is being passed", func() { + BeforeEach(func() { + fakeServiceBroker.SyslogDrainURL = "some-drain-url" + }) + + It("responds with the syslog drain url", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding_with_syslog.json"))) + }) + }) + + Context("when route_service_url is being passed", func() { + BeforeEach(func() { + fakeServiceBroker.RouteServiceURL = "some-route-url" + }) + + It("responds with the route service url", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding_with_route_service.json"))) + }) + }) + + Context("when a volume mount is being passed", func() { + BeforeEach(func() { + fakeServiceBroker.VolumeMounts = []brokerapi.VolumeMount{{ + Driver: "driver", + ContainerDir: "/dev/null", + Mode: "rw", + DeviceType: "shared", + Device: brokerapi.SharedDevice{ + VolumeId: "some-guid", + MountConfig: map[string]interface{}{"key": "value"}, + }, + }} + }) + + Context("when the broker API version is greater than 2.9", func() { + It("responds with a volume mount", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(fixture("binding_with_volume_mounts.json"))) + }) + }) + + Context("when the broker API version is 2.9", func() { + It("responds with an experimental volume mount", func() { + response := makeBindingRequestWithSpecificAPIVersion(uniqueInstanceID(), uniqueBindingID(), details, "2.9") + Expect(response.Body).To(MatchJSON(fixture("binding_with_experimental_volume_mounts.json"))) + }) + }) + + Context("when the broker API version is 2.8", func() { + It("responds with an experimental volume mount", func() { + response := makeBindingRequestWithSpecificAPIVersion(uniqueInstanceID(), uniqueBindingID(), details, "2.8") + Expect(response.Body).To(MatchJSON(fixture("binding_with_experimental_volume_mounts.json"))) + }) + }) + }) + + Context("when no bind details are being passed", func() { + It("returns a 422", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), nil) + Expect(response.StatusCode).To(Equal(http.StatusUnprocessableEntity)) + }) + }) + + Context("when there are arbitrary params", func() { + BeforeEach(func() { + details["parameters"] = map[string]interface{}{ + "string": "some-string", + "number": 1, + "object": struct{ Name string }{"some-name"}, + "array": []interface{}{"a", "b", "c"}, + } + }) + + It("calls Bind on the service broker with all params", func() { + rawParams := `{ + "string":"some-string", + "number":1, + "object": { "Name": "some-name" }, + "array": [ "a", "b", "c" ] + }` + makeBindingRequest(instanceID, bindingID, details) + Expect(string(fakeServiceBroker.BoundBindingDetails.RawParameters)).To(MatchJSON(rawParams)) + }) + }) + + Context("when there is a app_guid in the bind_resource", func() { + BeforeEach(func() { + details["bind_resource"] = map[string]interface{}{"app_guid": "a-guid"} + }) + + It("calls Bind on the service broker with the bind_resource", func() { + makeBindingRequest(instanceID, bindingID, details) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource).NotTo(BeNil()) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.AppGuid).To(Equal("a-guid")) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.Route).To(BeEmpty()) + }) + }) + + Context("when there is a route in the bind_resource", func() { + BeforeEach(func() { + details["bind_resource"] = map[string]interface{}{"route": "route.cf-apps.com"} + }) + + It("calls Bind on the service broker with the bind_resource", func() { + makeBindingRequest(instanceID, bindingID, details) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource).NotTo(BeNil()) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.Route).To(Equal("route.cf-apps.com")) + Expect(fakeServiceBroker.BoundBindingDetails.BindResource.AppGuid).To(BeEmpty()) + }) + }) + }) + + Context("when the associated instance does not exist", func() { + var instanceID string + + BeforeEach(func() { + fakeServiceBroker.BindError = brokerapi.ErrInstanceDoesNotExist + }) + + It("returns a 404", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(404)) + }) + + It("returns an error JSON object", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(`{"description":"instance does not exist"}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeBindingRequest(instanceID, uniqueBindingID(), details) + Expect(lastLogLine().Message).To(ContainSubstring(".bind.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + }) + }) + + Context("when the requested binding already exists", func() { + var instanceID string + + BeforeEach(func() { + fakeServiceBroker.BindError = brokerapi.ErrBindingAlreadyExists + }) + + It("returns a 409", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(409)) + }) + + It("returns an error JSON object", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(`{"description":"binding already exists"}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeBindingRequest(instanceID, uniqueBindingID(), details) + makeBindingRequest(instanceID, uniqueBindingID(), details) + + Expect(lastLogLine().Message).To(ContainSubstring(".bind.binding-already-exists")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("binding already exists")) + }) + }) + + Context("when the binding returns an unknown error", func() { + BeforeEach(func() { + fakeServiceBroker.BindError = errors.New("unknown error") + }) + + It("returns a generic 500 error response", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(500)) + Expect(response.Body).To(MatchJSON(`{"description":"unknown error"}`)) + }) + + It("logs a detailed error message", func() { + makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + + Expect(lastLogLine().Message).To(ContainSubstring(".bind.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("unknown error")) + }) + }) + + Context("when the binding returns a custom error", func() { + BeforeEach(func() { + fakeServiceBroker.BindError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeBindingRequest(uniqueInstanceID(), uniqueBindingID(), details) + Expect(lastLogLine().Message).To(ContainSubstring(".bind.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + + Describe("unbinding", func() { + makeUnbindingRequest := func(instanceID string, bindingID string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s/service_bindings/%s?plan_id=plan-id&service_id=service-id", + instanceID, bindingID) + request, _ := http.NewRequest("DELETE", path, strings.NewReader("")) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + Context("when the associated instance exists", func() { + var instanceID string + var provisionDetails map[string]interface{} + + BeforeEach(func() { + instanceID = uniqueInstanceID() + provisionDetails = map[string]interface{}{ + "plan_id": "plan-id", + "organization_guid": "organization-guid", + "space_guid": "space-guid", + } + makeInstanceProvisioningRequest(instanceID, provisionDetails, "") + }) + + Context("and the binding exists", func() { + var bindingID string + + BeforeEach(func() { + bindingID = uniqueBindingID() + makeBindingRequest(instanceID, bindingID, map[string]interface{}{}) + }) + + It("returns a 200", func() { + response := makeUnbindingRequest(instanceID, bindingID) + Expect(response.StatusCode).To(Equal(200)) + }) + + It("returns an empty JSON object", func() { + response := makeUnbindingRequest(instanceID, bindingID) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("contains plan_id", func() { + makeUnbindingRequest(instanceID, bindingID) + Expect(fakeServiceBroker.UnbindingDetails.PlanID).To(Equal("plan-id")) + }) + + It("contains service_id", func() { + makeUnbindingRequest(instanceID, bindingID) + Expect(fakeServiceBroker.UnbindingDetails.ServiceID).To(Equal("service-id")) + }) + }) + + Context("but the binding does not exist", func() { + It("returns a 410", func() { + response := makeUnbindingRequest(instanceID, "does-not-exist") + Expect(response.StatusCode).To(Equal(410)) + }) + + It("logs an appropriate error message", func() { + makeUnbindingRequest(instanceID, "does-not-exist") + + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.binding-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("binding does not exist")) + }) + + It("returns an empty JSON object", func() { + response := makeUnbindingRequest(instanceID, "does-not-exist") + Expect(response.Body).To(MatchJSON(`{}`)) + }) + }) + }) + + Context("when the associated instance does not exist", func() { + var instanceID string + + It("returns a 410", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.StatusCode).To(Equal(http.StatusGone)) + }) + + It("returns an empty JSON object", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + It("logs an appropriate error", func() { + instanceID = uniqueInstanceID() + makeUnbindingRequest(instanceID, uniqueBindingID()) + + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + }) + }) + + Context("when unbinding returns an unknown error", func() { + BeforeEach(func() { + fakeServiceBroker.UnbindError = errors.New("unknown error") + }) + + It("returns a generic 500 error response", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.StatusCode).To(Equal(500)) + Expect(response.Body).To(MatchJSON(`{"description":"unknown error"}`)) + }) + + It("logs a detailed error message", func() { + makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("unknown error")) + }) + }) + + Context("when unbinding returns a custom error", func() { + BeforeEach(func() { + fakeServiceBroker.UnbindError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeUnbindingRequest(uniqueInstanceID(), uniqueBindingID()) + Expect(lastLogLine().Message).To(ContainSubstring(".unbind.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + + Describe("last_operation", func() { + makeLastOperationRequest := func(instanceID, operationData string) *testflight.Response { + response := &testflight.Response{} + testflight.WithServer(brokerAPI, func(r *testflight.Requester) { + path := fmt.Sprintf("/v2/service_instances/%s/last_operation", instanceID) + if operationData != "" { + path = fmt.Sprintf("%s?operation=%s", path, url.QueryEscape(operationData)) + } + + request, _ := http.NewRequest("GET", path, strings.NewReader("")) + request.Header.Add("Content-Type", "application/json") + request.SetBasicAuth("username", "password") + + response = r.Do(request) + }) + return response + } + + It("calls the broker with the relevant instance ID", func() { + instanceID := "instanceID" + makeLastOperationRequest(instanceID, "") + Expect(fakeServiceBroker.LastOperationInstanceID).To(Equal(instanceID)) + }) + + It("calls the broker with the URL decoded operation data", func() { + instanceID := "an-instance" + operationData := `{"foo":"bar"}` + makeLastOperationRequest(instanceID, operationData) + Expect(fakeServiceBroker.LastOperationData).To(Equal(operationData)) + }) + + It("should return succeeded if the operation completed successfully", func() { + fakeServiceBroker.LastOperationState = "succeeded" + fakeServiceBroker.LastOperationDescription = "some description" + + instanceID := "instanceID" + response := makeLastOperationRequest(instanceID, "") + + logs := brokerLogger.Logs() + + Expect(logs[0].Message).To(ContainSubstring(".lastOperation.starting-check-for-operation")) + Expect(logs[0].Data["instance-id"]).To(ContainSubstring(instanceID)) + + Expect(logs[1].Message).To(ContainSubstring(".lastOperation.done-check-for-operation")) + Expect(logs[1].Data["instance-id"]).To(ContainSubstring(instanceID)) + Expect(logs[1].Data["state"]).To(ContainSubstring(string(fakeServiceBroker.LastOperationState))) + + Expect(response.StatusCode).To(Equal(200)) + Expect(response.Body).To(MatchJSON(fixture("last_operation_succeeded.json"))) + }) + + It("should return a 410 and log in case the instance id is not found", func() { + fakeServiceBroker.LastOperationError = brokerapi.ErrInstanceDoesNotExist + instanceID := "non-existing" + response := makeLastOperationRequest(instanceID, "") + + Expect(lastLogLine().Message).To(ContainSubstring(".lastOperation.instance-missing")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("instance does not exist")) + + Expect(response.StatusCode).To(Equal(410)) + Expect(response.Body).To(MatchJSON(`{}`)) + }) + + Context("when last_operation returns an unknown error", func() { + BeforeEach(func() { + fakeServiceBroker.LastOperationError = errors.New("unknown error") + }) + + It("returns a generic 500 error response", func() { + response := makeLastOperationRequest("instanceID", "") + + Expect(response.StatusCode).To(Equal(500)) + Expect(response.Body).To(MatchJSON(`{"description": "unknown error"}`)) + }) + + It("logs a detailed error message", func() { + makeLastOperationRequest("instanceID", "") + + Expect(lastLogLine().Message).To(ContainSubstring(".lastOperation.unknown-error")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("unknown error")) + }) + }) + + Context("when last_operation returns a custom error", func() { + BeforeEach(func() { + fakeServiceBroker.LastOperationError = brokerapi.NewFailureResponse( + errors.New("I failed in unique and interesting ways"), + http.StatusTeapot, + "interesting-failure", + ) + }) + + It("returns status teapot", func() { + response := makeLastOperationRequest("instanceID", "") + Expect(response.StatusCode).To(Equal(http.StatusTeapot)) + }) + + It("returns json with a description field and a useful error message", func() { + response := makeLastOperationRequest("instanceID", "") + Expect(response.Body).To(MatchJSON(`{"description":"I failed in unique and interesting ways"}`)) + }) + + It("logs an appropriate error", func() { + makeLastOperationRequest("instanceID", "") + Expect(lastLogLine().Message).To(ContainSubstring(".lastOperation.interesting-failure")) + Expect(lastLogLine().Data["error"]).To(ContainSubstring("I failed in unique and interesting ways")) + }) + }) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth.go new file mode 100644 index 000000000000..f9225e365710 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth.go @@ -0,0 +1,44 @@ +package auth + +import "net/http" + +type Wrapper struct { + username string + password string +} + +func NewWrapper(username, password string) *Wrapper { + return &Wrapper{ + username: username, + password: password, + } +} + +const notAuthorized = "Not Authorized" + +func (wrapper *Wrapper) Wrap(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !authorized(wrapper, r) { + http.Error(w, notAuthorized, http.StatusUnauthorized) + return + } + + handler.ServeHTTP(w, r) + }) +} + +func (wrapper *Wrapper) WrapFunc(handlerFunc http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !authorized(wrapper, r) { + http.Error(w, notAuthorized, http.StatusUnauthorized) + return + } + + handlerFunc(w, r) + }) +} + +func authorized(wrapper *Wrapper, r *http.Request) bool { + username, password, isOk := r.BasicAuth() + return isOk && username == wrapper.username && password == wrapper.password +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth_suite_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth_suite_test.go new file mode 100644 index 000000000000..e2d2a57d989a --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth_suite_test.go @@ -0,0 +1,13 @@ +package auth_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestAuth(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Auth Suite") +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth_test.go new file mode 100644 index 000000000000..da196c819b54 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/auth/auth_test.go @@ -0,0 +1,102 @@ +package auth_test + +import ( + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/pivotal-cf/brokerapi/auth" +) + +var _ = Describe("Auth Wrapper", func() { + var ( + username string + password string + httpRecorder *httptest.ResponseRecorder + ) + + newRequest := func(username, password string) *http.Request { + request, err := http.NewRequest("GET", "", nil) + Expect(err).NotTo(HaveOccurred()) + request.SetBasicAuth(username, password) + return request + } + + BeforeEach(func() { + username = "username" + password = "password" + httpRecorder = httptest.NewRecorder() + }) + + Describe("wrapped handler", func() { + var wrappedHandler http.Handler + + BeforeEach(func() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + }) + wrappedHandler = auth.NewWrapper(username, password).Wrap(handler) + }) + + It("works when the credentials are correct", func() { + request := newRequest(username, password) + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusCreated)) + }) + + It("fails when the username is empty", func() { + request := newRequest("", password) + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the password is empty", func() { + request := newRequest(username, "") + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the credentials are wrong", func() { + request := newRequest("thats", "apar") + wrappedHandler.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + }) + + Describe("wrapped handlerFunc", func() { + var wrappedHandlerFunc http.HandlerFunc + + BeforeEach(func() { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + }) + wrappedHandlerFunc = auth.NewWrapper(username, password).WrapFunc(handler) + }) + + It("works when the credentials are correct", func() { + request := newRequest(username, password) + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusCreated)) + }) + + It("fails when the username is empty", func() { + request := newRequest("", password) + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the password is empty", func() { + request := newRequest(username, "") + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + + It("fails when the credentials are wrong", func() { + request := newRequest("thats", "apar") + wrappedHandlerFunc.ServeHTTP(httpRecorder, request) + Expect(httpRecorder.Code).To(Equal(http.StatusUnauthorized)) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/catalog.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/catalog.go new file mode 100644 index 000000000000..4ff6b7257acf --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/catalog.go @@ -0,0 +1,65 @@ +package brokerapi + +type Service struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Bindable bool `json:"bindable"` + Tags []string `json:"tags,omitempty"` + PlanUpdatable bool `json:"plan_updateable"` + Plans []ServicePlan `json:"plans"` + Requires []RequiredPermission `json:"requires,omitempty"` + Metadata *ServiceMetadata `json:"metadata,omitempty"` + DashboardClient *ServiceDashboardClient `json:"dashboard_client,omitempty"` +} + +type ServiceDashboardClient struct { + ID string `json:"id"` + Secret string `json:"secret"` + RedirectURI string `json:"redirect_uri"` +} + +type ServicePlan struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Free *bool `json:"free,omitempty"` + Bindable *bool `json:"bindable,omitempty"` + Metadata *ServicePlanMetadata `json:"metadata,omitempty"` +} + +type ServicePlanMetadata struct { + DisplayName string `json:"displayName,omitempty"` + Bullets []string `json:"bullets,omitempty"` + Costs []ServicePlanCost `json:"costs,omitempty"` +} + +type ServicePlanCost struct { + Amount map[string]float64 `json:"amount"` + Unit string `json:"unit"` +} + +type ServiceMetadata struct { + DisplayName string `json:"displayName,omitempty"` + ImageUrl string `json:"imageUrl,omitempty"` + LongDescription string `json:"longDescription,omitempty"` + ProviderDisplayName string `json:"providerDisplayName,omitempty"` + DocumentationUrl string `json:"documentationUrl,omitempty"` + SupportUrl string `json:"supportUrl,omitempty"` +} + +func FreeValue(v bool) *bool { + return &v +} + +func BindableValue(v bool) *bool { + return &v +} + +type RequiredPermission string + +const ( + PermissionRouteForwarding = RequiredPermission("route_forwarding") + PermissionSyslogDrain = RequiredPermission("syslog_drain") + PermissionVolumeMount = RequiredPermission("volume_mount") +) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/catalog_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/catalog_test.go new file mode 100644 index 000000000000..cde1bda2cc69 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/catalog_test.go @@ -0,0 +1,163 @@ +package brokerapi_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pivotal-cf/brokerapi" +) + +var _ = Describe("Catalog", func() { + Describe("Service", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + service := brokerapi.Service{ + ID: "ID-1", + Name: "Cassandra", + Description: "A Cassandra Plan", + Bindable: true, + Plans: []brokerapi.ServicePlan{}, + Metadata: &brokerapi.ServiceMetadata{}, + Tags: []string{"test"}, + PlanUpdatable: true, + DashboardClient: &brokerapi.ServiceDashboardClient{ + ID: "Dashboard ID", + Secret: "dashboardsecret", + RedirectURI: "the.dashboa.rd", + }, + } + jsonString := `{ + "id":"ID-1", + "name":"Cassandra", + "description":"A Cassandra Plan", + "bindable":true, + "plan_updateable":true, + "tags":["test"], + "plans":[], + "dashboard_client":{ + "id":"Dashboard ID", + "secret":"dashboardsecret", + "redirect_uri":"the.dashboa.rd" + }, + "metadata":{ + + } + }` + Expect(json.Marshal(service)).To(MatchJSON(jsonString)) + }) + }) + + It("encodes the optional 'requires' fields", func() { + service := brokerapi.Service{ + ID: "ID-1", + Name: "Cassandra", + Description: "A Cassandra Plan", + Bindable: true, + Plans: []brokerapi.ServicePlan{}, + Metadata: &brokerapi.ServiceMetadata{}, + Tags: []string{"test"}, + PlanUpdatable: true, + Requires: []brokerapi.RequiredPermission{ + brokerapi.PermissionRouteForwarding, + brokerapi.PermissionSyslogDrain, + brokerapi.PermissionVolumeMount, + }, + DashboardClient: &brokerapi.ServiceDashboardClient{ + ID: "Dashboard ID", + Secret: "dashboardsecret", + RedirectURI: "the.dashboa.rd", + }, + } + jsonString := `{ + "id":"ID-1", + "name":"Cassandra", + "description":"A Cassandra Plan", + "bindable":true, + "plan_updateable":true, + "tags":["test"], + "plans":[], + "requires": ["route_forwarding", "syslog_drain", "volume_mount"], + "dashboard_client":{ + "id":"Dashboard ID", + "secret":"dashboardsecret", + "redirect_uri":"the.dashboa.rd" + }, + "metadata":{ + + } + }` + Expect(json.Marshal(service)).To(MatchJSON(jsonString)) + }) + }) + + Describe("ServicePlan", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + plan := brokerapi.ServicePlan{ + ID: "ID-1", + Name: "Cassandra", + Description: "A Cassandra Plan", + Bindable: brokerapi.BindableValue(true), + Free: brokerapi.FreeValue(true), + Metadata: &brokerapi.ServicePlanMetadata{ + Bullets: []string{"hello", "its me"}, + DisplayName: "name", + }, + } + jsonString := `{ + "id":"ID-1", + "name":"Cassandra", + "description":"A Cassandra Plan", + "free": true, + "bindable": true, + "metadata":{ + "bullets":["hello", "its me"], + "displayName":"name" + } + }` + + Expect(json.Marshal(plan)).To(MatchJSON(jsonString)) + }) + }) + }) + + Describe("ServicePlanMetadata", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + metadata := brokerapi.ServicePlanMetadata{ + Bullets: []string{"test"}, + DisplayName: "Some display name", + } + jsonString := `{"bullets":["test"],"displayName":"Some display name"}` + + Expect(json.Marshal(metadata)).To(MatchJSON(jsonString)) + }) + }) + }) + + Describe("ServiceMetadata", func() { + Describe("JSON encoding", func() { + It("uses the correct keys", func() { + metadata := brokerapi.ServiceMetadata{ + DisplayName: "Cassandra", + LongDescription: "A long description of Cassandra", + DocumentationUrl: "doc", + SupportUrl: "support", + ImageUrl: "image", + ProviderDisplayName: "display", + } + jsonString := `{ + "displayName":"Cassandra", + "longDescription":"A long description of Cassandra", + "documentationUrl":"doc", + "supportUrl":"support", + "imageUrl":"image", + "providerDisplayName":"display" + }` + + Expect(json.Marshal(metadata)).To(MatchJSON(jsonString)) + }) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/failure_response.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/failure_response.go new file mode 100644 index 000000000000..2fc7501b70ef --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/failure_response.go @@ -0,0 +1,113 @@ +// Copyright (C) 2016-Present Pivotal Software, Inc. All rights reserved. +// This program and the accompanying materials are made available under the terms of the 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 brokerapi + +import ( + "net/http" + + "fmt" + + "code.cloudfoundry.org/lager" +) + +// FailureResponse can be returned from any of the `ServiceBroker` interface methods +// which allow an error to be returned. Doing so will provide greater control over +// the HTTP response. +type FailureResponse struct { + error + statusCode int + loggerAction string + emptyResponse bool + errorKey string +} + +// NewFailureResponse returns a pointer to a new instance of FailureResponse. +// err will by default be used as both a logging message and HTTP response description. +// statusCode is the HTTP status code to be returned, must be 4xx or 5xx +// loggerAction is a short description which will be used as the action if the error is logged. +func NewFailureResponse(err error, statusCode int, loggerAction string) *FailureResponse { + return &FailureResponse{ + error: err, + statusCode: statusCode, + loggerAction: loggerAction, + } +} + +// ErrorResponse returns an interface{} which will be JSON encoded and form the body +// of the HTTP response +func (f *FailureResponse) ErrorResponse() interface{} { + if f.emptyResponse { + return EmptyResponse{} + } + + return ErrorResponse{ + Description: f.error.Error(), + Error: f.errorKey, + } +} + +// ValidatedStatusCode returns the HTTP response status code. If the code is not 4xx +// or 5xx, an InternalServerError will be returned instead. +func (f *FailureResponse) ValidatedStatusCode(logger lager.Logger) int { + if f.statusCode < 400 || 600 <= f.statusCode { + if logger != nil { + logger.Error("validating-status-code", fmt.Errorf("Invalid failure http response code: 600, expected 4xx or 5xx, returning internal server error: 500.")) + } + return http.StatusInternalServerError + } + return f.statusCode +} + +// LoggerAction returns the loggerAction, used as the action when logging +func (f *FailureResponse) LoggerAction() string { + return f.loggerAction +} + +// FailureResponseBuilder provides a fluent set of methods to build a *FailureResponse. +type FailureResponseBuilder struct { + error + statusCode int + loggerAction string + emptyResponse bool + errorKey string +} + +// NewFailureResponseBuilder returns a pointer to a newly instantiated FailureResponseBuilder +// Accepts required arguments to create a FailureResponse. +func NewFailureResponseBuilder(err error, statusCode int, loggerAction string) *FailureResponseBuilder { + return &FailureResponseBuilder{ + error: err, + statusCode: statusCode, + loggerAction: loggerAction, + emptyResponse: false, + } +} + +// WithErrorKey adds a custom ErrorKey which will be used in FailureResponse to add an `Error` +// field to the JSON HTTP response body +func (f *FailureResponseBuilder) WithErrorKey(errorKey string) *FailureResponseBuilder { + f.errorKey = errorKey + return f +} + +// WithEmptyResponse will cause the built FailureResponse to return an empty JSON object as the +// HTTP response body +func (f *FailureResponseBuilder) WithEmptyResponse() *FailureResponseBuilder { + f.emptyResponse = true + return f +} + +// Build returns the generated FailureResponse built using previously configured variables. +func (f *FailureResponseBuilder) Build() *FailureResponse { + return &FailureResponse{ + error: f.error, + statusCode: f.statusCode, + loggerAction: f.loggerAction, + emptyResponse: f.emptyResponse, + errorKey: f.errorKey, + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/failure_response_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/failure_response_test.go new file mode 100644 index 000000000000..0475d8e92493 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/failure_response_test.go @@ -0,0 +1,82 @@ +package brokerapi_test + +import ( + "github.com/pivotal-cf/brokerapi" + + "errors" + + "net/http" + + "code.cloudfoundry.org/lager" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("FailureResponse", func() { + Describe("ErrorResponse", func() { + It("returns a ErrorResponse containing the error message", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), http.StatusForbidden, "log-key") + Expect(failureResponse.ErrorResponse()).To(Equal(brokerapi.ErrorResponse{ + Description: "my error message", + })) + }) + + Context("when the error key is provided", func() { + It("returns a ErrorResponse containing the error message and the error key", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithErrorKey("error key").Build() + Expect(failureResponse.ErrorResponse()).To(Equal(brokerapi.ErrorResponse{ + Description: "my error message", + Error: "error key", + })) + }) + }) + + Context("when created with empty response", func() { + It("returns an EmptyResponse", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithEmptyResponse().Build() + Expect(failureResponse.ErrorResponse()).To(Equal(brokerapi.EmptyResponse{})) + }) + }) + }) + + Describe("ValidatedStatusCode", func() { + It("returns the status code that was passed in", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), http.StatusForbidden, "log-key") + Expect(failureResponse.ValidatedStatusCode(nil)).To(Equal(http.StatusForbidden)) + }) + + It("when error key is provided it returns the status code that was passed in", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithErrorKey("error key").Build() + Expect(failureResponse.ValidatedStatusCode(nil)).To(Equal(http.StatusForbidden)) + }) + + Context("when the status code is invalid", func() { + It("returns 500", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), 600, "log-key") + Expect(failureResponse.ValidatedStatusCode(nil)).To(Equal(http.StatusInternalServerError)) + }) + + It("logs that the status has been changed", func() { + log := gbytes.NewBuffer() + logger := lager.NewLogger("test") + logger.RegisterSink(lager.NewWriterSink(log, lager.DEBUG)) + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), 600, "log-key") + failureResponse.ValidatedStatusCode(logger) + Expect(log).To(gbytes.Say("Invalid failure http response code: 600, expected 4xx or 5xx, returning internal server error: 500.")) + }) + }) + }) + + Describe("LoggerAction", func() { + It("returns the logger action that was passed in", func() { + failureResponse := brokerapi.NewFailureResponseBuilder(errors.New("my error message"), http.StatusForbidden, "log-key").WithErrorKey("error key").Build() + Expect(failureResponse.LoggerAction()).To(Equal("log-key")) + }) + + It("when error key is provided it returns the logger action that was passed in", func() { + failureResponse := brokerapi.NewFailureResponse(errors.New("my error message"), http.StatusForbidden, "log-key") + Expect(failureResponse.LoggerAction()).To(Equal("log-key")) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fakes/fake_service_broker.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fakes/fake_service_broker.go new file mode 100644 index 000000000000..9c44a50f38ca --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fakes/fake_service_broker.go @@ -0,0 +1,324 @@ +package fakes + +import ( + "context" + + "github.com/pivotal-cf/brokerapi" +) + +type FakeServiceBroker struct { + ProvisionDetails brokerapi.ProvisionDetails + UpdateDetails brokerapi.UpdateDetails + DeprovisionDetails brokerapi.DeprovisionDetails + + ProvisionedInstanceIDs []string + DeprovisionedInstanceIDs []string + UpdatedInstanceIDs []string + + BoundInstanceIDs []string + BoundBindingIDs []string + BoundBindingDetails brokerapi.BindDetails + SyslogDrainURL string + RouteServiceURL string + VolumeMounts []brokerapi.VolumeMount + + UnbindingDetails brokerapi.UnbindDetails + + InstanceLimit int + + ProvisionError error + BindError error + UnbindError error + DeprovisionError error + LastOperationError error + UpdateError error + + BrokerCalled bool + LastOperationState brokerapi.LastOperationState + LastOperationDescription string + + AsyncAllowed bool + + ShouldReturnAsync bool + DashboardURL string + OperationDataToReturn string + + LastOperationInstanceID string + LastOperationData string + + ReceivedContext bool +} + +type FakeAsyncServiceBroker struct { + FakeServiceBroker + ShouldProvisionAsync bool +} + +type FakeAsyncOnlyServiceBroker struct { + FakeServiceBroker +} + +func (fakeBroker *FakeServiceBroker) Services(context context.Context) []brokerapi.Service { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + return []brokerapi.Service{ + { + ID: "0A789746-596F-4CEA-BFAC-A0795DA056E3", + Name: "p-cassandra", + Description: "Cassandra service for application development and testing", + Bindable: true, + PlanUpdatable: true, + Plans: []brokerapi.ServicePlan{ + { + ID: "ABE176EE-F69F-4A96-80CE-142595CC24E3", + Name: "default", + Description: "The default Cassandra plan", + Metadata: &brokerapi.ServicePlanMetadata{ + Bullets: []string{}, + DisplayName: "Cassandra", + }, + }, + }, + Metadata: &brokerapi.ServiceMetadata{ + DisplayName: "Cassandra", + LongDescription: "Long description", + DocumentationUrl: "http://thedocs.com", + SupportUrl: "http://helpme.no", + }, + Tags: []string{ + "pivotal", + "cassandra", + }, + }, + } +} + +func (fakeBroker *FakeServiceBroker) Provision(context context.Context, instanceID string, details brokerapi.ProvisionDetails, asyncAllowed bool) (brokerapi.ProvisionedServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.ProvisionError != nil { + return brokerapi.ProvisionedServiceSpec{}, fakeBroker.ProvisionError + } + + if len(fakeBroker.ProvisionedInstanceIDs) >= fakeBroker.InstanceLimit { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceLimitMet + } + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceAlreadyExists + } + + fakeBroker.ProvisionDetails = details + fakeBroker.ProvisionedInstanceIDs = append(fakeBroker.ProvisionedInstanceIDs, instanceID) + return brokerapi.ProvisionedServiceSpec{DashboardURL: fakeBroker.DashboardURL}, nil +} + +func (fakeBroker *FakeAsyncServiceBroker) Provision(context context.Context, instanceID string, details brokerapi.ProvisionDetails, asyncAllowed bool) (brokerapi.ProvisionedServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.ProvisionError != nil { + return brokerapi.ProvisionedServiceSpec{}, fakeBroker.ProvisionError + } + + if len(fakeBroker.ProvisionedInstanceIDs) >= fakeBroker.InstanceLimit { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceLimitMet + } + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceAlreadyExists + } + + fakeBroker.ProvisionDetails = details + fakeBroker.ProvisionedInstanceIDs = append(fakeBroker.ProvisionedInstanceIDs, instanceID) + return brokerapi.ProvisionedServiceSpec{IsAsync: fakeBroker.ShouldProvisionAsync, DashboardURL: fakeBroker.DashboardURL, OperationData: fakeBroker.OperationDataToReturn}, nil +} + +func (fakeBroker *FakeAsyncOnlyServiceBroker) Provision(context context.Context, instanceID string, details brokerapi.ProvisionDetails, asyncAllowed bool) (brokerapi.ProvisionedServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.ProvisionError != nil { + return brokerapi.ProvisionedServiceSpec{}, fakeBroker.ProvisionError + } + + if len(fakeBroker.ProvisionedInstanceIDs) >= fakeBroker.InstanceLimit { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceLimitMet + } + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrInstanceAlreadyExists + } + + if !asyncAllowed { + return brokerapi.ProvisionedServiceSpec{}, brokerapi.ErrAsyncRequired + } + + fakeBroker.ProvisionDetails = details + fakeBroker.ProvisionedInstanceIDs = append(fakeBroker.ProvisionedInstanceIDs, instanceID) + return brokerapi.ProvisionedServiceSpec{IsAsync: true, DashboardURL: fakeBroker.DashboardURL}, nil +} + +func (fakeBroker *FakeServiceBroker) Update(context context.Context, instanceID string, details brokerapi.UpdateDetails, asyncAllowed bool) (brokerapi.UpdateServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.UpdateError != nil { + return brokerapi.UpdateServiceSpec{}, fakeBroker.UpdateError + } + + fakeBroker.UpdateDetails = details + fakeBroker.UpdatedInstanceIDs = append(fakeBroker.UpdatedInstanceIDs, instanceID) + fakeBroker.AsyncAllowed = asyncAllowed + return brokerapi.UpdateServiceSpec{IsAsync: fakeBroker.ShouldReturnAsync, OperationData: fakeBroker.OperationDataToReturn}, nil +} + +func (fakeBroker *FakeServiceBroker) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.DeprovisionError != nil { + return brokerapi.DeprovisionServiceSpec{}, fakeBroker.DeprovisionError + } + + fakeBroker.DeprovisionDetails = details + fakeBroker.DeprovisionedInstanceIDs = append(fakeBroker.DeprovisionedInstanceIDs, instanceID) + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.DeprovisionServiceSpec{}, nil + } + return brokerapi.DeprovisionServiceSpec{IsAsync: false}, brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeAsyncOnlyServiceBroker) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.DeprovisionError != nil { + return brokerapi.DeprovisionServiceSpec{IsAsync: true}, fakeBroker.DeprovisionError + } + + if !asyncAllowed { + return brokerapi.DeprovisionServiceSpec{IsAsync: true}, brokerapi.ErrAsyncRequired + } + + fakeBroker.DeprovisionedInstanceIDs = append(fakeBroker.DeprovisionedInstanceIDs, instanceID) + fakeBroker.DeprovisionDetails = details + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.DeprovisionServiceSpec{IsAsync: true, OperationData: fakeBroker.OperationDataToReturn}, nil + } + + return brokerapi.DeprovisionServiceSpec{IsAsync: true, OperationData: fakeBroker.OperationDataToReturn}, brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeAsyncServiceBroker) Deprovision(context context.Context, instanceID string, details brokerapi.DeprovisionDetails, asyncAllowed bool) (brokerapi.DeprovisionServiceSpec, error) { + fakeBroker.BrokerCalled = true + + if fakeBroker.DeprovisionError != nil { + return brokerapi.DeprovisionServiceSpec{IsAsync: asyncAllowed}, fakeBroker.DeprovisionError + } + + fakeBroker.DeprovisionedInstanceIDs = append(fakeBroker.DeprovisionedInstanceIDs, instanceID) + fakeBroker.DeprovisionDetails = details + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + return brokerapi.DeprovisionServiceSpec{IsAsync: asyncAllowed, OperationData: fakeBroker.OperationDataToReturn}, nil + } + + return brokerapi.DeprovisionServiceSpec{OperationData: fakeBroker.OperationDataToReturn, IsAsync: asyncAllowed}, brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeServiceBroker) Bind(context context.Context, instanceID, bindingID string, details brokerapi.BindDetails) (brokerapi.Binding, error) { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.BindError != nil { + return brokerapi.Binding{}, fakeBroker.BindError + } + + fakeBroker.BoundBindingDetails = details + + fakeBroker.BoundInstanceIDs = append(fakeBroker.BoundInstanceIDs, instanceID) + fakeBroker.BoundBindingIDs = append(fakeBroker.BoundBindingIDs, bindingID) + + return brokerapi.Binding{ + Credentials: FakeCredentials{ + Host: "127.0.0.1", + Port: 3000, + Username: "batman", + Password: "robin", + }, + SyslogDrainURL: fakeBroker.SyslogDrainURL, + RouteServiceURL: fakeBroker.RouteServiceURL, + VolumeMounts: fakeBroker.VolumeMounts, + }, nil +} + +func (fakeBroker *FakeServiceBroker) Unbind(context context.Context, instanceID, bindingID string, details brokerapi.UnbindDetails) error { + fakeBroker.BrokerCalled = true + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.UnbindError != nil { + return fakeBroker.UnbindError + } + + fakeBroker.UnbindingDetails = details + + if sliceContains(instanceID, fakeBroker.ProvisionedInstanceIDs) { + if sliceContains(bindingID, fakeBroker.BoundBindingIDs) { + return nil + } + return brokerapi.ErrBindingDoesNotExist + } + + return brokerapi.ErrInstanceDoesNotExist +} + +func (fakeBroker *FakeServiceBroker) LastOperation(context context.Context, instanceID, operationData string) (brokerapi.LastOperation, error) { + fakeBroker.LastOperationInstanceID = instanceID + fakeBroker.LastOperationData = operationData + + if val, ok := context.Value("test_context").(bool); ok { + fakeBroker.ReceivedContext = val + } + + if fakeBroker.LastOperationError != nil { + return brokerapi.LastOperation{}, fakeBroker.LastOperationError + } + + return brokerapi.LastOperation{State: fakeBroker.LastOperationState, Description: fakeBroker.LastOperationDescription}, nil +} + +type FakeCredentials struct { + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` +} + +func sliceContains(needle string, haystack []string) bool { + for _, element := range haystack { + if element == needle { + return true + } + } + return false +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/async_required.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/async_required.json new file mode 100644 index 000000000000..e3c4593728b1 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/async_required.json @@ -0,0 +1,4 @@ +{ + "error": "AsyncRequired", + "description": "This service plan requires client support for asynchronous service operations." +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding.json new file mode 100644 index 000000000000..c6d5afaa29a8 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding.json @@ -0,0 +1,8 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + } +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_experimental_volume_mounts.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_experimental_volume_mounts.json new file mode 100644 index 000000000000..2f234412625a --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_experimental_volume_mounts.json @@ -0,0 +1,17 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "volume_mounts": [{ + "container_path": "/dev/null", + "mode": "rw", + "private": { + "driver": "driver", + "group_id": "some-guid", + "config": "{\"key\":\"value\"}" + } + }] +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_route_service.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_route_service.json new file mode 100644 index 000000000000..87264a5d0e16 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_route_service.json @@ -0,0 +1,9 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "route_service_url": "some-route-url" +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_syslog.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_syslog.json new file mode 100644 index 000000000000..3e8caae9b610 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_syslog.json @@ -0,0 +1,9 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "syslog_drain_url": "some-drain-url" +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_volume_mounts.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_volume_mounts.json new file mode 100644 index 000000000000..6b56b90b0fa8 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/binding_with_volume_mounts.json @@ -0,0 +1,20 @@ +{ + "credentials": { + "host": "127.0.0.1", + "port": 3000, + "username": "batman", + "password": "robin" + }, + "volume_mounts": [{ + "driver": "driver", + "container_dir": "/dev/null", + "mode": "rw", + "device_type": "shared", + "device": { + "volume_id": "some-guid", + "mount_config": { + "key": "value" + } + } + }] +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/catalog.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/catalog.json new file mode 100644 index 000000000000..bf6c71cdf407 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/catalog.json @@ -0,0 +1,27 @@ +{ + "services": [{ + "bindable": true, + "description": "Cassandra service for application development and testing", + "id": "0A789746-596F-4CEA-BFAC-A0795DA056E3", + "name": "p-cassandra", + "plan_updateable": true, + "plans": [{ + "description": "The default Cassandra plan", + "id": "ABE176EE-F69F-4A96-80CE-142595CC24E3", + "name": "default", + "metadata": { + "displayName": "Cassandra" + } + }], + "metadata": { + "displayName": "Cassandra", + "longDescription": "Long description", + "documentationUrl": "http://thedocs.com", + "supportUrl": "http://helpme.no" + }, + "tags": [ + "pivotal", + "cassandra" + ] + }] +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/instance_limit_error.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/instance_limit_error.json new file mode 100644 index 000000000000..df66adbcd4d2 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/instance_limit_error.json @@ -0,0 +1,3 @@ +{ + "description": "instance limit for this service has been reached" +} \ No newline at end of file diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/invalid_async_provision_error.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/invalid_async_provision_error.json new file mode 100644 index 000000000000..4507fe973fff --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/invalid_async_provision_error.json @@ -0,0 +1,3 @@ +{ + "description": "broker attempted to provision asynchronously when not supported by the caller" +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/last_operation_succeeded.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/last_operation_succeeded.json new file mode 100644 index 000000000000..5d9a5a43fc06 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/last_operation_succeeded.json @@ -0,0 +1,4 @@ +{ + "state": "succeeded", + "description": "some description" +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/operation_data_response.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/operation_data_response.json new file mode 100644 index 000000000000..e5f86ae28caf --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/operation_data_response.json @@ -0,0 +1,3 @@ +{ + "operation": "some-operation-data" +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning.json @@ -0,0 +1 @@ +{} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning_with_dashboard.json b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning_with_dashboard.json new file mode 100644 index 000000000000..0a31f48bd308 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/fixtures/provisioning_with_dashboard.json @@ -0,0 +1,3 @@ +{ + "dashboard_url": "some-dashboard-url" +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/response.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/response.go new file mode 100644 index 000000000000..64ada789d37f --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/response.go @@ -0,0 +1,49 @@ +package brokerapi + +type EmptyResponse struct{} + +type ErrorResponse struct { + Error string `json:"error,omitempty"` + Description string `json:"description"` +} + +type CatalogResponse struct { + Services []Service `json:"services"` +} + +type ProvisioningResponse struct { + DashboardURL string `json:"dashboard_url,omitempty"` + OperationData string `json:"operation,omitempty"` +} + +type UpdateResponse struct { + OperationData string `json:"operation,omitempty"` +} + +type DeprovisionResponse struct { + OperationData string `json:"operation,omitempty"` +} + +type LastOperationResponse struct { + State LastOperationState `json:"state"` + Description string `json:"description,omitempty"` +} + +type ExperimentalVolumeMountBindingResponse struct { + Credentials interface{} `json:"credentials"` + SyslogDrainURL string `json:"syslog_drain_url,omitempty"` + RouteServiceURL string `json:"route_service_url,omitempty"` + VolumeMounts []ExperimentalVolumeMount `json:"volume_mounts,omitempty"` +} + +type ExperimentalVolumeMount struct { + ContainerPath string `json:"container_path"` + Mode string `json:"mode"` + Private ExperimentalVolumeMountPrivate `json:"private"` +} + +type ExperimentalVolumeMountPrivate struct { + Driver string `json:"driver"` + GroupID string `json:"group_id"` + Config string `json:"config"` +} diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/response_test.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/response_test.go new file mode 100644 index 000000000000..d024296d8d0a --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/response_test.go @@ -0,0 +1,70 @@ +package brokerapi_test + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pivotal-cf/brokerapi" +) + +var _ = Describe("Catalog Response", func() { + Describe("JSON encoding", func() { + It("has a list of services", func() { + catalogResponse := brokerapi.CatalogResponse{ + Services: []brokerapi.Service{}, + } + jsonString := `{"services":[]}` + + Expect(json.Marshal(catalogResponse)).To(MatchJSON(jsonString)) + }) + }) +}) + +var _ = Describe("Provisioning Response", func() { + Describe("JSON encoding", func() { + Context("when the dashboard URL is not present", func() { + It("does not return it in the JSON", func() { + provisioningResponse := brokerapi.ProvisioningResponse{} + jsonString := `{}` + + Expect(json.Marshal(provisioningResponse)).To(MatchJSON(jsonString)) + }) + }) + + Context("when the dashboard URL is present", func() { + It("returns it in the JSON", func() { + provisioningResponse := brokerapi.ProvisioningResponse{ + DashboardURL: "http://example.com/broker", + } + jsonString := `{"dashboard_url":"http://example.com/broker"}` + + Expect(json.Marshal(provisioningResponse)).To(MatchJSON(jsonString)) + }) + }) + }) +}) + +var _ = Describe("Binding Response", func() { + Describe("JSON encoding", func() { + It("has a credentials object", func() { + binding := brokerapi.Binding{} + jsonString := `{"credentials":null}` + + Expect(json.Marshal(binding)).To(MatchJSON(jsonString)) + }) + }) +}) + +var _ = Describe("Error Response", func() { + Describe("JSON encoding", func() { + It("has a description field", func() { + errorResponse := brokerapi.ErrorResponse{ + Description: "a bad thing happened", + } + jsonString := `{"description":"a bad thing happened"}` + + Expect(json.Marshal(errorResponse)).To(MatchJSON(jsonString)) + }) + }) +}) diff --git a/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/service_broker.go b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/service_broker.go new file mode 100644 index 000000000000..01d83c4715b7 --- /dev/null +++ b/cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/vendor/github.com/pivotal-cf/brokerapi/service_broker.go @@ -0,0 +1,187 @@ +package brokerapi + +import ( + "context" + "encoding/json" + "errors" + "net/http" +) + +type ServiceBroker interface { + Services(context context.Context) []Service + + Provision(context context.Context, instanceID string, details ProvisionDetails, asyncAllowed bool) (ProvisionedServiceSpec, error) + Deprovision(context context.Context, instanceID string, details DeprovisionDetails, asyncAllowed bool) (DeprovisionServiceSpec, error) + + Bind(context context.Context, instanceID, bindingID string, details BindDetails) (Binding, error) + Unbind(context context.Context, instanceID, bindingID string, details UnbindDetails) error + + Update(context context.Context, instanceID string, details UpdateDetails, asyncAllowed bool) (UpdateServiceSpec, error) + + LastOperation(context context.Context, instanceID, operationData string) (LastOperation, error) +} + +type DetailsWithRawParameters interface { + GetRawParameters() json.RawMessage +} + +func (d ProvisionDetails) GetRawParameters() json.RawMessage { + return d.RawParameters +} + +func (d BindDetails) GetRawParameters() json.RawMessage { + return d.RawParameters +} + +func (d UpdateDetails) GetRawParameters() json.RawMessage { + return d.RawParameters +} + +type ProvisionDetails struct { + ServiceID string `json:"service_id"` + PlanID string `json:"plan_id"` + OrganizationGUID string `json:"organization_guid"` + SpaceGUID string `json:"space_guid"` + RawParameters json.RawMessage `json:"parameters,omitempty"` +} + +type ProvisionedServiceSpec struct { + IsAsync bool + DashboardURL string + OperationData string +} + +type BindDetails struct { + AppGUID string `json:"app_guid"` + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` + BindResource *BindResource `json:"bind_resource,omitempty"` + RawParameters json.RawMessage `json:"parameters,omitempty"` +} + +type BindResource struct { + AppGuid string `json:"app_guid,omitempty"` + Route string `json:"route,omitempty"` +} + +type UnbindDetails struct { + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` +} + +type UpdateServiceSpec struct { + IsAsync bool + OperationData string +} + +type DeprovisionServiceSpec struct { + IsAsync bool + OperationData string +} + +type DeprovisionDetails struct { + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` +} + +type UpdateDetails struct { + ServiceID string `json:"service_id"` + PlanID string `json:"plan_id"` + RawParameters json.RawMessage `json:"parameters,omitempty"` + PreviousValues PreviousValues `json:"previous_values"` +} + +type PreviousValues struct { + PlanID string `json:"plan_id"` + ServiceID string `json:"service_id"` + OrgID string `json:"organization_id"` + SpaceID string `json:"space_id"` +} + +type LastOperation struct { + State LastOperationState + Description string +} + +type LastOperationState string + +const ( + InProgress LastOperationState = "in progress" + Succeeded LastOperationState = "succeeded" + Failed LastOperationState = "failed" +) + +type Binding struct { + Credentials interface{} `json:"credentials"` + SyslogDrainURL string `json:"syslog_drain_url,omitempty"` + RouteServiceURL string `json:"route_service_url,omitempty"` + VolumeMounts []VolumeMount `json:"volume_mounts,omitempty"` +} + +type VolumeMount struct { + Driver string `json:"driver"` + ContainerDir string `json:"container_dir"` + Mode string `json:"mode"` + DeviceType string `json:"device_type"` + Device SharedDevice `json:"device"` +} + +type SharedDevice struct { + VolumeId string `json:"volume_id"` + MountConfig map[string]interface{} `json:"mount_config"` +} + +const ( + instanceExistsMsg = "instance already exists" + instanceDoesntExistMsg = "instance does not exist" + serviceLimitReachedMsg = "instance limit for this service has been reached" + servicePlanQuotaExceededMsg = "The quota for this service plan has been exceeded. Please contact your Operator for help." + serviceQuotaExceededMsg = "The quota for this service has been exceeded. Please contact your Operator for help." + bindingExistsMsg = "binding already exists" + bindingDoesntExistMsg = "binding does not exist" + asyncRequiredMsg = "This service plan requires client support for asynchronous service operations." + planChangeUnsupportedMsg = "The requested plan migration cannot be performed" + rawInvalidParamsMsg = "The format of the parameters is not valid JSON" + appGuidMissingMsg = "app_guid is a required field but was not provided" +) + +var ( + ErrInstanceAlreadyExists = NewFailureResponseBuilder( + errors.New(instanceExistsMsg), http.StatusConflict, instanceAlreadyExistsErrorKey, + ).WithEmptyResponse().Build() + + ErrInstanceDoesNotExist = NewFailureResponseBuilder( + errors.New(instanceDoesntExistMsg), http.StatusGone, instanceMissingErrorKey, + ).WithEmptyResponse().Build() + + ErrInstanceLimitMet = NewFailureResponse( + errors.New(serviceLimitReachedMsg), http.StatusInternalServerError, instanceLimitReachedErrorKey, + ) + + ErrBindingAlreadyExists = NewFailureResponse( + errors.New(bindingExistsMsg), http.StatusConflict, bindingAlreadyExistsErrorKey, + ) + + ErrBindingDoesNotExist = NewFailureResponseBuilder( + errors.New(bindingDoesntExistMsg), http.StatusGone, bindingMissingErrorKey, + ).WithEmptyResponse().Build() + + ErrAsyncRequired = NewFailureResponseBuilder( + errors.New(asyncRequiredMsg), http.StatusUnprocessableEntity, asyncRequiredKey, + ).WithErrorKey("AsyncRequired").Build() + + ErrPlanChangeNotSupported = NewFailureResponseBuilder( + errors.New(planChangeUnsupportedMsg), http.StatusUnprocessableEntity, planChangeNotSupportedKey, + ).WithErrorKey("PlanChangeNotSupported").Build() + + ErrRawParamsInvalid = NewFailureResponse( + errors.New(rawInvalidParamsMsg), http.StatusUnprocessableEntity, invalidRawParamsKey, + ) + + ErrAppGuidNotProvided = NewFailureResponse( + errors.New(appGuidMissingMsg), http.StatusUnprocessableEntity, appGuidNotProvidedErrorKey, + ) + + ErrPlanQuotaExceeded = errors.New(servicePlanQuotaExceededMsg) + ErrServiceQuotaExceeded = errors.New(serviceQuotaExceededMsg) +) diff --git a/origin.spec b/origin.spec index eeafd52ec5df..39c6a221defb 100644 --- a/origin.spec +++ b/origin.spec @@ -312,7 +312,6 @@ ln -s hypercc %{buildroot}%{_bindir}/cluster-capacity # Install service-catalog install -p -m 755 cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/_output/local/bin/${PLATFORM}/apiserver %{buildroot}%{_bindir}/ install -p -m 755 cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/_output/local/bin/${PLATFORM}/controller-manager %{buildroot}%{_bindir}/ -install -p -m 755 cmd/service-catalog/go/src/github.com/kubernetes-incubator/service-catalog/_output/local/bin/${PLATFORM}/user-broker %{buildroot}%{_bindir}/ # Install pod install -p -m 755 _output/local/bin/${PLATFORM}/pod %{buildroot}%{_bindir}/ @@ -561,7 +560,6 @@ fi %files service-catalog %{_bindir}/apiserver %{_bindir}/controller-manager -%{_bindir}/user-broker %files -n tuned-profiles-%{name}-node %license LICENSE diff --git a/tools/rebasehelpers/commitchecker/commitchecker.go b/tools/rebasehelpers/commitchecker/commitchecker.go index 662398d9e0fc..66ef7cef1388 100644 --- a/tools/rebasehelpers/commitchecker/commitchecker.go +++ b/tools/rebasehelpers/commitchecker/commitchecker.go @@ -27,9 +27,11 @@ func main() { // TODO: Filter out bump commits for now until we decide how to deal with // them correctly. + // TODO: ...along with subtree merges. nonbumpCommits := []util.Commit{} for _, commit := range commits { - if !strings.HasPrefix(commit.Summary, "bump(") { + lastDescriptionLine := commit.Description[len(commit.Description)-1] + if !strings.HasPrefix(commit.Summary, "bump(") && !strings.HasPrefix(lastDescriptionLine, "git-subtree-split:") { nonbumpCommits = append(nonbumpCommits, commit) } } diff --git a/tools/rebasehelpers/util/git.go b/tools/rebasehelpers/util/git.go index 38d919309123..16407571c92d 100644 --- a/tools/rebasehelpers/util/git.go +++ b/tools/rebasehelpers/util/git.go @@ -31,9 +31,10 @@ var SupportedHosts = map[string]int{ } type Commit struct { - Sha string - Summary string - Files []File + Sha string + Summary string + Description []string + Files []File } func (c Commit) DeclaresUpstreamChange() bool { @@ -214,12 +215,17 @@ func CommitsBetween(a, b string) ([]Commit, error) { func NewCommitFromOnelineLog(log string) (Commit, error) { var commit Commit + var err error parts := strings.Split(log, " ") if len(parts) < 2 { return commit, fmt.Errorf("invalid log entry: %s", log) } commit.Sha = parts[0] commit.Summary = strings.Join(parts[1:], " ") + commit.Description, err = descriptionInCommit(commit.Sha) + if err != nil { + return commit, err + } files, err := filesInCommit(commit.Sha) if err != nil { return commit, err @@ -335,6 +341,22 @@ func filesInCommit(sha string) ([]File, error) { return files, nil } +func descriptionInCommit(sha string) ([]string, error) { + descriptionLines := []string{} + stdout, stderr, err := run("git", "show", "--quiet", sha) + if err != nil { + return descriptionLines, fmt.Errorf("%s: %s", stderr, err) + } + + for _, commitLine := range strings.Split(stdout, "\n") { + if len(commitLine) == 0 { + continue + } + descriptionLines = append(descriptionLines, strings.Trim(commitLine, " ")) + } + return descriptionLines, nil +} + func run(args ...string) (string, string, error) { cmd := exec.Command(args[0], args[1:]...) var stdout bytes.Buffer