diff --git a/cmd/minikube/cmd/config/config.go b/cmd/minikube/cmd/config/config.go index 6ed1b06d2628ed1d451bb5c0b895317174acd6e5..26d37402ea998b269a812259b14643d4fcb064f1 100644 --- a/cmd/minikube/cmd/config/config.go +++ b/cmd/minikube/cmd/config/config.go @@ -130,6 +130,12 @@ var settings = []Setting{ validations: []setFn{IsValidAddon}, callbacks: []setFn{EnableOrDisableAddon}, }, + { + name: "heapster", + set: SetBool, + validations: []setFn{IsValidAddon}, + callbacks: []setFn{EnableOrDisableAddon}, + }, } var ConfigCmd = &cobra.Command{ diff --git a/cmd/minikube/cmd/config/open.go b/cmd/minikube/cmd/config/open.go new file mode 100644 index 0000000000000000000000000000000000000000..ebe034e316b9bfb8eb717b2024066a9ba24b9dc6 --- /dev/null +++ b/cmd/minikube/cmd/config/open.go @@ -0,0 +1,117 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + "os" + "text/template" + "time" + + "github.com/docker/machine/libmachine" + "github.com/spf13/cobra" + "k8s.io/minikube/pkg/minikube/assets" + "k8s.io/minikube/pkg/minikube/cluster" + "k8s.io/minikube/pkg/minikube/constants" + "k8s.io/minikube/pkg/util" +) + +var ( + namespace string + https bool + addonsURLMode bool + addonsURLFormat string + addonsURLTemplate *template.Template +) + +const defaultAddonsFormatTemplate = "http://{{.IP}}:{{.Port}}" + +var addonsOpenCmd = &cobra.Command{ + Use: "open ADDON_NAME", + Short: "Opens the addon w/ADDON_NAME within minikube (example: minikube addons open dashboard). For a list of available addons use: minikube addons list ", + Long: "Opens the addon w/ADDON_NAME within minikube (example: minikube addons open dashboard). For a list of available addons use: minikube addons list ", + PreRun: func(cmd *cobra.Command, args []string) { + t, err := template.New("addonsURL").Parse(addonsURLFormat) + if err != nil { + fmt.Fprintln(os.Stderr, "The value passed to --format is invalid:\n\n", err) + os.Exit(1) + } + addonsURLTemplate = t + }, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + fmt.Fprintln(os.Stderr, "usage: minikube addons open ADDON_NAME") + os.Exit(1) + } + addonName := args[0] + api := libmachine.NewClient(constants.Minipath, constants.MakeMiniPath("certs")) + defer api.Close() + + cluster.EnsureMinikubeRunningOrExit(api, 1) + addon, ok := assets.Addons[addonName] // validate addon input + if !ok { + fmt.Fprintln(os.Stderr, fmt.Sprintf(`addon '%s' is not a valid addon packaged with minikube. +To see the list of available addons run: +minikube addons list`, addonName)) + os.Exit(1) + } + ok, err := addon.IsEnabled() + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + if !ok { + fmt.Fprintln(os.Stderr, fmt.Sprintf(`addon '%s' is currently not enabled. +To enable this addon run: +minikube addons enable %s`, addonName, addonName)) + os.Exit(1) + } + + namespace := "kube-system" + key := "kubernetes.io/minikube-addons-endpoint" + if err := util.RetryAfter(20, + func() error { + _, err := cluster.GetServiceListByLabel(namespace, key, addonName) + if err != nil { + return err + } + return nil + }, 6*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "Could not find endpoint for addon %s: %s\n", addonName, err) + os.Exit(1) + } + serviceList, err := cluster.GetServiceListByLabel(namespace, key, addonName) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting service with namespace :%s and labels %s:%s: %s", namespace, key, addonName, err) + os.Exit(1) + } + for i := range serviceList.Items { + service := serviceList.Items[i].ObjectMeta.Name + cluster.WaitAndMaybeOpenService(api, namespace, service, addonsURLTemplate, addonsURLMode, https) + + } + }, +} + +func init() { + AddonsCmd.AddCommand(addonsOpenCmd) + addonsOpenCmd.Flags().BoolVar(&addonsURLMode, "url", false, "Display the kubernetes addons URL in the CLI instead of opening it in the default browser") + addonsOpenCmd.Flags().BoolVar(&https, "https", false, "Open the addons URL with https instead of http") + + addonsOpenCmd.PersistentFlags().StringVar(&addonsURLFormat, "format", defaultAddonsFormatTemplate, "Format to output addons URL in. This format will be applied to each url individually and they will be printed one at a time.") + AddonsCmd.AddCommand(addonsOpenCmd) +} diff --git a/cmd/minikube/cmd/dashboard.go b/cmd/minikube/cmd/dashboard.go index 55e79f5d0d19a3720cb02cbdca5d7c4633c32d21..063c5cba06aaeff759f8563bec464adf184ebd8a 100644 --- a/cmd/minikube/cmd/dashboard.go +++ b/cmd/minikube/cmd/dashboard.go @@ -49,7 +49,7 @@ var dashboardCmd = &cobra.Command{ namespace := "kube-system" service := "kubernetes-dashboard" - if err := commonutil.RetryAfter(20, func() error { return CheckService(namespace, service) }, 6*time.Second); err != nil { + if err := commonutil.RetryAfter(20, func() error { return cluster.CheckService(namespace, service) }, 6*time.Second); err != nil { fmt.Fprintf(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s\n", service, err) os.Exit(1) } diff --git a/cmd/minikube/cmd/service.go b/cmd/minikube/cmd/service.go index 897b221cdcd0ba2bd66a78b3e423ea046e1d3c57..c2b88bbb321e0641cdbb28710625e073a915ebcb 100644 --- a/cmd/minikube/cmd/service.go +++ b/cmd/minikube/cmd/service.go @@ -19,18 +19,13 @@ package cmd import ( "fmt" "os" - "strings" "text/template" - "time" "github.com/docker/machine/libmachine" - "github.com/pkg/browser" "github.com/pkg/errors" "github.com/spf13/cobra" - kubeapi "k8s.io/kubernetes/pkg/api" "k8s.io/minikube/pkg/minikube/cluster" "k8s.io/minikube/pkg/minikube/constants" - "k8s.io/minikube/pkg/util" ) var ( @@ -46,7 +41,7 @@ var serviceCmd = &cobra.Command{ Use: "service [flags] SERVICE", Short: "Gets the kubernetes URL(s) for the specified service in your local cluster", Long: `Gets the kubernetes URL(s) for the specified service in your local cluster. In the case of multiple URLs they will be printed one at a time`, - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PreRun: func(cmd *cobra.Command, args []string) { t, err := template.New("serviceURL").Parse(serviceURLFormat) if err != nil { fmt.Fprintln(os.Stderr, "The value passed to --format is invalid:\n\n", err) @@ -71,28 +66,7 @@ var serviceCmd = &cobra.Command{ service, namespace)) os.Exit(1) } - if err := util.RetryAfter(20, func() error { return CheckService(namespace, service) }, 6*time.Second); err != nil { - fmt.Fprintf(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s\n", service, err) - os.Exit(1) - } - - urls, err := cluster.GetServiceURLsForService(api, namespace, service, serviceURLTemplate) - if err != nil { - fmt.Fprintln(os.Stderr, err) - fmt.Fprintln(os.Stderr, "Check that minikube is running and that you have specified the correct namespace (-n flag).") - os.Exit(1) - } - for _, url := range urls { - if https { - url = strings.Replace(url, "http", "https", 1) - } - if serviceURLMode || !strings.HasPrefix(url, "http") { - fmt.Fprintln(os.Stdout, url) - } else { - fmt.Fprintln(os.Stdout, "Opening kubernetes service "+namespace+"/"+service+" in default browser...") - browser.OpenURL(url) - } - } + cluster.WaitAndMaybeOpenService(api, namespace, service, serviceURLTemplate, serviceURLMode, https) }, } @@ -119,37 +93,3 @@ func validateService(namespace string, service string) error { } return nil } - -// CheckService waits for the specified service to be ready by returning an error until the service is up -// The check is done by polling the endpoint associated with the service and when the endpoint exists, returning no error->service-online -func CheckService(namespace string, service string) error { - client, err := cluster.GetKubernetesClient() - if err != nil { - return &util.RetriableError{Err: err} - } - endpoints := client.Endpoints(namespace) - if err != nil { - return &util.RetriableError{Err: err} - } - endpoint, err := endpoints.Get(service) - if err != nil { - return &util.RetriableError{Err: err} - } - return CheckEndpointReady(endpoint) -} - -const notReadyMsg = "Waiting, endpoint for service is not ready yet...\n" - -func CheckEndpointReady(endpoint *kubeapi.Endpoints) error { - if len(endpoint.Subsets) == 0 { - fmt.Fprintf(os.Stderr, notReadyMsg) - return &util.RetriableError{Err: errors.New("Endpoint for service is not ready yet")} - } - for _, subset := range endpoint.Subsets { - if len(subset.Addresses) == 0 { - fmt.Fprintf(os.Stderr, notReadyMsg) - return &util.RetriableError{Err: errors.New("No endpoints for service are ready yet")} - } - } - return nil -} diff --git a/cmd/minikube/cmd/service_test.go b/cmd/minikube/cmd/service_test.go deleted file mode 100644 index 6b2f1c141fc55597da3dbc7cbcf8808a10cd7a4b..0000000000000000000000000000000000000000 --- a/cmd/minikube/cmd/service_test.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cmd - -import ( - "testing" - - kubeApi "k8s.io/kubernetes/pkg/api" -) - -func TestCheckEndpointReady(t *testing.T) { - endpointNoSubsets := &kubeApi.Endpoints{} - if err := CheckEndpointReady(endpointNoSubsets); err == nil { - t.Fatalf("Endpoint had no subsets but CheckEndpointReady did not return an error") - } - - endpointNotReady := &kubeApi.Endpoints{ - Subsets: []kubeApi.EndpointSubset{ - {Addresses: []kubeApi.EndpointAddress{}, - NotReadyAddresses: []kubeApi.EndpointAddress{ - {IP: "1.1.1.1"}, - {IP: "2.2.2.2"}, - {IP: "3.3.3.3"}, - }}}} - if err := CheckEndpointReady(endpointNotReady); err == nil { - t.Fatalf("Endpoint had no Addresses but CheckEndpointReady did not return an error") - } - - endpointReady := &kubeApi.Endpoints{ - Subsets: []kubeApi.EndpointSubset{ - {Addresses: []kubeApi.EndpointAddress{ - {IP: "1.1.1.1"}, - {IP: "2.2.2.2"}, - }, - NotReadyAddresses: []kubeApi.EndpointAddress{}, - }}, - } - if err := CheckEndpointReady(endpointReady); err != nil { - t.Fatalf("Endpoint was ready with at least one Address, but CheckEndpointReady returned an error") - } -} diff --git a/deploy/addons/addon-manager.yaml b/deploy/addons/addon-manager.yaml index ddabfa3e45200d8b02c2970a23a789318b9c18ee..ba11af8b8c3d50cb740cc649a9e09dff8b76a724 100644 --- a/deploy/addons/addon-manager.yaml +++ b/deploy/addons/addon-manager.yaml @@ -20,6 +20,7 @@ metadata: labels: component: kube-addon-manager version: v5.1 + kubernetes.io/minikube-addons: addon-manager spec: hostNetwork: true containers: diff --git a/deploy/addons/dashboard-rc.yaml b/deploy/addons/dashboard-rc.yaml index 176723695f2db580f92d09a0e609578ece869b13..c6e688f14d749fd347d5178a4a20620743b02e05 100644 --- a/deploy/addons/dashboard-rc.yaml +++ b/deploy/addons/dashboard-rc.yaml @@ -21,6 +21,7 @@ metadata: app: kubernetes-dashboard version: v1.4.2 kubernetes.io/cluster-service: "true" + kubernetes.io/minikube-addons: dashboard spec: replicas: 1 selector: diff --git a/deploy/addons/dashboard-svc.yaml b/deploy/addons/dashboard-svc.yaml index b6a69b3456fa72bc30a6c5773db49cf74d99c7da..f2e70ea809958eae34f8b624d5ae2bbdb7bd1be0 100644 --- a/deploy/addons/dashboard-svc.yaml +++ b/deploy/addons/dashboard-svc.yaml @@ -21,6 +21,8 @@ metadata: labels: app: kubernetes-dashboard kubernetes.io/cluster-service: "true" + kubernetes.io/minikube-addons: dashboard + kubernetes.io/minikube-addons-endpoint: dashboard spec: type: NodePort ports: diff --git a/deploy/addons/grafana-svc.yaml b/deploy/addons/grafana-svc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1762bf9ec4c04fc4b17add4f2d851640a6664b54 --- /dev/null +++ b/deploy/addons/grafana-svc.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + kubernetes.io/cluster-service: 'true' + kubernetes.io/name: monitoring-grafana + kubernetes.io/minikube-addons: heapster + kubernetes.io/minikube-addons-endpoint: heapster + name: monitoring-grafana + namespace: kube-system +spec: + type: NodePort + ports: + - port: 80 + targetPort: 3000 + selector: + kubernetes.io/cluster-service: "true" + name: influxGrafana diff --git a/deploy/addons/heapster-rc.yaml b/deploy/addons/heapster-rc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f45f0d2d63ea3eeb79d0ab9e9fb891c375eb20e9 --- /dev/null +++ b/deploy/addons/heapster-rc.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + labels: + kubernetes.io/cluster-service: 'true' + k8s-app: heapster + name: heapster + kubernetes.io/minikube-addons: heapster + name: heapster + namespace: kube-system +spec: + replicas: 1 + selector: + kubernetes.io/cluster-service: "true" + k8s-app: heapster + template: + metadata: + labels: + kubernetes.io/cluster-service: 'true' + k8s-app: heapster + spec: + containers: + - name: heapster + image: gcr.io/google_containers/heapster:v1.2.0 + imagePullPolicy: IfNotPresent + command: + - /heapster + - --source=kubernetes + - --sink=influxdb:http://monitoring-influxdb:8086 + - --metric_resolution=60s + volumeMounts: + - name: ssl-certs + mountPath: /etc/ssl/certs + readOnly: true + volumes: + - name: ssl-certs + hostPath: + path: /etc/ssl/certs diff --git a/deploy/addons/heapster-svc.yaml b/deploy/addons/heapster-svc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c264b9a77ac9e0809c8cbd298e5acd4b2cbcbb14 --- /dev/null +++ b/deploy/addons/heapster-svc.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + kubernetes.io/cluster-service: 'true' + kubernetes.io/name: heapster + kubernetes.io/minikube-addons: heapster + name: heapster + namespace: kube-system +spec: + ports: + - port: 80 + targetPort: 8082 + selector: + kubernetes.io/cluster-service: "true" + k8s-app: heapster diff --git a/deploy/addons/influxGrafana-rc.yaml b/deploy/addons/influxGrafana-rc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e65d80e68759fe220252bcd67d7776075b7cb7fd --- /dev/null +++ b/deploy/addons/influxGrafana-rc.yaml @@ -0,0 +1,67 @@ +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ReplicationController +metadata: + labels: + kubernetes.io/cluster-service: 'true' + name: influxGrafana + kubernetes.io/minikube-addons: heapster + name: influxdb-grafana + namespace: kube-system +spec: + replicas: 1 + selector: + kubernetes.io/cluster-service: "true" + name: influxGrafana + template: + metadata: + labels: + kubernetes.io/cluster-service: 'true' + name: influxGrafana + spec: + containers: + - name: influxdb + image: kubernetes/heapster_influxdb:v0.6 + imagePullPolicy: IfNotPresent + volumeMounts: + - mountPath: /data + name: influxdb-storage + - name: grafana + image: gcr.io/google_containers/heapster_grafana:v2.6.0-2 + imagePullPolicy: IfNotPresent + env: + - name: INFLUXDB_SERVICE_URL + value: http://localhost:8086 + # The following env variables are required to make Grafana accessible via + # the kubernetes api-server proxy. On production clusters, we recommend + # removing these env variables, setup auth for grafana, and expose the grafana + # service using a LoadBalancer or a public IP. + - name: GF_AUTH_BASIC_ENABLED + value: "false" + - name: GF_AUTH_ANONYMOUS_ENABLED + value: "true" + - name: GF_AUTH_ANONYMOUS_ORG_ROLE + value: Admin + - name: GF_SERVER_ROOT_URL + value: / + volumeMounts: + - mountPath: /var + name: grafana-storage + volumes: + - name: influxdb-storage + emptyDir: {} + - name: grafana-storage + emptyDir: {} diff --git a/deploy/addons/influxdb-svc.yaml b/deploy/addons/influxdb-svc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c11bfd14f5d5cd170ea6bc6667bc7084fe992857 --- /dev/null +++ b/deploy/addons/influxdb-svc.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + kubernetes.io/cluster-service: 'true' + kubernetes.io/name: monitoring-influxdb + kubernetes.io/minikube-addons: heapster + name: monitoring-influxdb + namespace: kube-system +spec: + ports: + - name: http + port: 8083 + targetPort: 8083 + - name: api + port: 8086 + targetPort: 8086 + selector: + kubernetes.io/cluster-service: "true" + name: influxGrafana diff --git a/docs/bash-completion b/docs/bash-completion index 15b993222ff7a6195785d07d77c02b47bd364291..fe4572a089399485ab5c721ffb5f4878d6ad60fd 100644 --- a/docs/bash-completion +++ b/docs/bash-completion @@ -315,6 +315,68 @@ _minikube_addons_list() noun_aliases=() } +_minikube_addons_open() +{ + last_command="minikube_addons_open" + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--format=") + flags+=("--https") + local_nonpersistent_flags+=("--https") + flags+=("--url") + local_nonpersistent_flags+=("--url") + flags+=("--alsologtostderr") + flags+=("--log_backtrace_at=") + flags+=("--log_dir=") + flags+=("--logtostderr") + flags+=("--show-libmachine-logs") + flags+=("--stderrthreshold=") + flags+=("--v=") + two_word_flags+=("-v") + flags+=("--vmodule=") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_minikube_addons_open() +{ + last_command="minikube_addons_open" + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--format=") + flags+=("--https") + local_nonpersistent_flags+=("--https") + flags+=("--url") + local_nonpersistent_flags+=("--url") + flags+=("--alsologtostderr") + flags+=("--log_backtrace_at=") + flags+=("--log_dir=") + flags+=("--logtostderr") + flags+=("--show-libmachine-logs") + flags+=("--stderrthreshold=") + flags+=("--v=") + two_word_flags+=("-v") + flags+=("--vmodule=") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _minikube_addons() { last_command="minikube_addons" @@ -322,6 +384,8 @@ _minikube_addons() commands+=("disable") commands+=("enable") commands+=("list") + commands+=("open") + commands+=("open") flags=() two_word_flags=() diff --git a/docs/minikube_addons.md b/docs/minikube_addons.md index 56524596d8e62851f7a06cc1a36e80613577af04..b094802a151a4f6c0b82dedf312e8b6d62f37920 100644 --- a/docs/minikube_addons.md +++ b/docs/minikube_addons.md @@ -37,4 +37,6 @@ For the list of accessible variables for the template, see the struct values her * [minikube addons disable](minikube_addons_disable.md) - Disables the addon w/ADDON_NAME within minikube (example: minikube addons disable dashboard). For a list of available addons use: minikube addons list * [minikube addons enable](minikube_addons_enable.md) - Enables the addon w/ADDON_NAME within minikube (example: minikube addons enable dashboard). For a list of available addons use: minikube addons list * [minikube addons list](minikube_addons_list.md) - Lists all available minikube addons as well as there current status (enabled/disabled) +* [minikube addons open](minikube_addons_open.md) - Opens the addon w/ADDON_NAME within minikube (example: minikube addons open dashboard). For a list of available addons use: minikube addons list +* [minikube addons open](minikube_addons_open.md) - Opens the addon w/ADDON_NAME within minikube (example: minikube addons open dashboard). For a list of available addons use: minikube addons list diff --git a/docs/minikube_addons_open.md b/docs/minikube_addons_open.md new file mode 100644 index 0000000000000000000000000000000000000000..bc3560fb0a057a0e5650eafd250dfccd8d6338bb --- /dev/null +++ b/docs/minikube_addons_open.md @@ -0,0 +1,37 @@ +## minikube addons open + +Opens the addon w/ADDON_NAME within minikube (example: minikube addons open dashboard). For a list of available addons use: minikube addons list + +### Synopsis + + +Opens the addon w/ADDON_NAME within minikube (example: minikube addons open dashboard). For a list of available addons use: minikube addons list + +``` +minikube addons open ADDON_NAME +``` + +### Options + +``` + --format string Format to output addons URL in. This format will be applied to each url individually and they will be printed one at a time. (default "http://{{.IP}}:{{.Port}}") + --https Open the addons URL with https instead of http + --url Display the kubernetes addons URL in the CLI instead of opening it in the default browser +``` + +### Options inherited from parent commands + +``` + --alsologtostderr log to standard error as well as files + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory (default "") + --logtostderr log to standard error instead of files + --show-libmachine-logs Whether or not to show logs from libmachine. + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + -v, --v Level log level for V logs + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO +* [minikube addons](minikube_addons.md) - Modify minikube's kubernetes addons + diff --git a/docs/minikube_config.md b/docs/minikube_config.md index ec84403e5398eb081f2825ceba939ef30cc858ce..bed3baf1e1fd33bb600aabcdffb0113a459616d5 100644 --- a/docs/minikube_config.md +++ b/docs/minikube_config.md @@ -25,6 +25,7 @@ Configurable fields: * dashboard * addon-manager * kube-dns + * heapster ``` minikube config SUBCOMMAND [flags] diff --git a/pkg/minikube/assets/addons.go b/pkg/minikube/assets/addons.go index 34edac8edc7731a3905dba01779b63b097f3154e..2204ea1a272f2d10cd76a9530228a4deba2c0dde 100644 --- a/pkg/minikube/assets/addons.go +++ b/pkg/minikube/assets/addons.go @@ -86,6 +86,33 @@ var Addons = map[string]*Addon{ "kube-dns-svc.yaml", "0640"), }, true, "kube-dns"), + "heapster": NewAddon([]*MemoryAsset{ + NewMemoryAsset( + "deploy/addons/influxGrafana-rc.yaml", + constants.AddonsPath, + "influxGrafana-rc.yaml", + "0640"), + NewMemoryAsset( + "deploy/addons/grafana-svc.yaml", + constants.AddonsPath, + "grafana-svc.yaml", + "0640"), + NewMemoryAsset( + "deploy/addons/influxdb-svc.yaml", + constants.AddonsPath, + "influxdb-svc.yaml", + "0640"), + NewMemoryAsset( + "deploy/addons/heapster-rc.yaml", + constants.AddonsPath, + "heapster-rc.yaml", + "0640"), + NewMemoryAsset( + "deploy/addons/heapster-svc.yaml", + constants.AddonsPath, + "heapster-svc.yaml", + "0640"), + }, false, "heapster"), } func AddMinikubeAddonsDirToAssets(assetList *[]CopyableFile) { diff --git a/pkg/minikube/cluster/cluster.go b/pkg/minikube/cluster/cluster.go index c772479a31db57d0614c43ba975379aaa16eb20d..cfd23b1da576ce584ddc3c84a0b3b276caafd387 100644 --- a/pkg/minikube/cluster/cluster.go +++ b/pkg/minikube/cluster/cluster.go @@ -39,10 +39,12 @@ import ( "github.com/docker/machine/libmachine/state" "github.com/golang/glog" download "github.com/jimmidyson/go-download" + "github.com/pkg/browser" "github.com/pkg/errors" kubeapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + "k8s.io/kubernetes/pkg/labels" "k8s.io/minikube/pkg/minikube/assets" "k8s.io/minikube/pkg/minikube/constants" @@ -683,3 +685,85 @@ func GetServiceURLs(api libmachine.API, namespace string, t *template.Template) return serviceURLs, nil } + +// CheckService waits for the specified service to be ready by returning an error until the service is up +// The check is done by polling the endpoint associated with the service and when the endpoint exists, returning no error->service-online +func CheckService(namespace string, service string) error { + client, err := GetKubernetesClient() + if err != nil { + return &util.RetriableError{Err: err} + } + endpoints := client.Endpoints(namespace) + if err != nil { + return &util.RetriableError{Err: err} + } + endpoint, err := endpoints.Get(service) + if err != nil { + return &util.RetriableError{Err: err} + } + return checkEndpointReady(endpoint) +} + +func checkEndpointReady(endpoint *kubeapi.Endpoints) error { + const notReadyMsg = "Waiting, endpoint for service is not ready yet...\n" + if len(endpoint.Subsets) == 0 { + fmt.Fprintf(os.Stderr, notReadyMsg) + return &util.RetriableError{Err: errors.New("Endpoint for service is not ready yet")} + } + for _, subset := range endpoint.Subsets { + if len(subset.Addresses) == 0 { + fmt.Fprintf(os.Stderr, notReadyMsg) + return &util.RetriableError{Err: errors.New("No endpoints for service are ready yet")} + } + } + return nil +} + +func WaitAndMaybeOpenService(api libmachine.API, namespace string, service string, urlTemplate *template.Template, urlMode bool, https bool) { + if err := util.RetryAfter(20, func() error { return CheckService(namespace, service) }, 6*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "Could not find finalized endpoint being pointed to by %s: %s\n", service, err) + os.Exit(1) + } + + urls, err := GetServiceURLsForService(api, namespace, service, urlTemplate) + if err != nil { + fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, "Check that minikube is running and that you have specified the correct namespace (-n flag).") + os.Exit(1) + } + for _, url := range urls { + if https { + url = strings.Replace(url, "http", "https", 1) + } + if urlMode || !strings.HasPrefix(url, "http") { + fmt.Fprintln(os.Stdout, url) + } else { + fmt.Fprintln(os.Stdout, "Opening kubernetes service "+namespace+"/"+service+" in default browser...") + browser.OpenURL(url) + } + } +} + +func GetServiceListByLabel(namespace string, key string, value string) (*kubeapi.ServiceList, error) { + client, err := GetKubernetesClient() + if err != nil { + return &kubeapi.ServiceList{}, &util.RetriableError{Err: err} + } + services := client.Services(namespace) + if err != nil { + return &kubeapi.ServiceList{}, &util.RetriableError{Err: err} + } + return getServiceListFromServicesByLabel(services, key, value) +} + +func getServiceListFromServicesByLabel(services unversioned.ServiceInterface, key string, value string) (*kubeapi.ServiceList, error) { + selector := labels.SelectorFromSet(labels.Set(map[string]string{key: value})) + serviceList, err := services.List(kubeapi.ListOptions{LabelSelector: selector}) + if err != nil { + return &kubeapi.ServiceList{}, &util.RetriableError{Err: err} + } + if len(serviceList.Items) == 0 { + return &kubeapi.ServiceList{}, &util.RetriableError{Err: err} + } + return serviceList, nil +} diff --git a/pkg/minikube/cluster/cluster_test.go b/pkg/minikube/cluster/cluster_test.go index 5636ab8a5a4b6c4217aa89089737d298b800100e..9b445d3d5ddb17de6fabb411c4a6aad05fe680d0 100644 --- a/pkg/minikube/cluster/cluster_test.go +++ b/pkg/minikube/cluster/cluster_test.go @@ -34,6 +34,8 @@ import ( "github.com/pkg/errors" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/watch" "k8s.io/minikube/pkg/minikube/assets" "k8s.io/minikube/pkg/minikube/constants" "k8s.io/minikube/pkg/minikube/tests" @@ -779,3 +781,97 @@ func TestUpdateCustomAddons(t *testing.T) { t.Fatalf("Custom addon not copied. Expected transfers to contain custom addon with content: %s. It was: %s", testContent2, transferred) } } + +func TestCheckEndpointReady(t *testing.T) { + endpointNoSubsets := &api.Endpoints{} + if err := checkEndpointReady(endpointNoSubsets); err == nil { + t.Fatalf("Endpoint had no subsets but checkEndpointReady did not return an error") + } + + endpointNotReady := &api.Endpoints{ + Subsets: []api.EndpointSubset{ + {Addresses: []api.EndpointAddress{}, + NotReadyAddresses: []api.EndpointAddress{ + {IP: "1.1.1.1"}, + {IP: "2.2.2.2"}, + {IP: "3.3.3.3"}, + }}}} + if err := checkEndpointReady(endpointNotReady); err == nil { + t.Fatalf("Endpoint had no Addresses but checkEndpointReady did not return an error") + } + + endpointReady := &api.Endpoints{ + Subsets: []api.EndpointSubset{ + {Addresses: []api.EndpointAddress{ + {IP: "1.1.1.1"}, + {IP: "2.2.2.2"}, + }, + NotReadyAddresses: []api.EndpointAddress{}, + }}, + } + if err := checkEndpointReady(endpointReady); err != nil { + t.Fatalf("Endpoint was ready with at least one Address, but checkEndpointReady returned an error") + } +} + +type ServiceInterfaceMock struct { + ServiceList *api.ServiceList +} + +func (s ServiceInterfaceMock) List(opts api.ListOptions) (*api.ServiceList, error) { + serviceList := &api.ServiceList{ + Items: []api.Service{}, + } + keyValArr := strings.Split(opts.LabelSelector.String(), "=") + for _, service := range s.ServiceList.Items { + if service.Spec.Selector[keyValArr[0]] == keyValArr[1] { + serviceList.Items = append(serviceList.Items, service) + } + } + return serviceList, nil +} +func (s ServiceInterfaceMock) Get(name string) (*api.Service, error) { + return nil, nil +} +func (s ServiceInterfaceMock) Create(*api.Service) (*api.Service, error) { + return nil, nil +} +func (s ServiceInterfaceMock) Update(*api.Service) (*api.Service, error) { + return nil, nil +} +func (s ServiceInterfaceMock) UpdateStatus(*api.Service) (*api.Service, error) { + return nil, nil +} +func (s ServiceInterfaceMock) Delete(name string) error { + return nil +} +func (s ServiceInterfaceMock) Watch(opts api.ListOptions) (watch.Interface, error) { + return nil, nil +} +func (s ServiceInterfaceMock) ProxyGet(scheme, name, port, path string, params map[string]string) restclient.ResponseWrapper { + return nil +} + +func TestGetServiceListFromServicesByLabel(t *testing.T) { + serviceList := &api.ServiceList{ + Items: []api.Service{ + { + Spec: api.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + } + serviceIface := ServiceInterfaceMock{ + ServiceList: serviceList, + } + if _, err := getServiceListFromServicesByLabel(serviceIface, "shouldError", "shouldError"); err == nil { + t.Fatalf("Service had no label match but getServiceListFromServicesByLabel did not return an error") + } + + if _, err := getServiceListFromServicesByLabel(serviceIface, "foo", "bar"); err != nil { + t.Fatalf("Endpoint was ready with at least one Address, but getServiceListFromServicesByLabel returned an error") + } +}