service.go 10.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/*
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 service

import (
	"bytes"
P
Predrag Rogic 已提交
21
	"context"
22
	"fmt"
23
	"io"
24
	"net/url"
25
	"os"
26
	"strconv"
27
	"strings"
28
	"text/template"
29 30 31
	"time"

	"github.com/docker/machine/libmachine"
32
	"github.com/olekukonko/tablewriter"
33
	"github.com/pkg/errors"
34
	core "k8s.io/api/core/v1"
35
	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
36
	"k8s.io/apimachinery/pkg/labels"
37
	typed_core "k8s.io/client-go/kubernetes/typed/core/v1"
38
	"k8s.io/klog/v2"
39
	"k8s.io/minikube/pkg/kapi"
40
	"k8s.io/minikube/pkg/minikube/machine"
S
Sharif Elgamal 已提交
41
	"k8s.io/minikube/pkg/minikube/out"
42
	"k8s.io/minikube/pkg/minikube/style"
M
Medya Gh 已提交
43
	"k8s.io/minikube/pkg/util/retry"
44 45
)

46 47
const (
	// DefaultWait is the default wait time, in seconds
48
	DefaultWait = 2
49
	// DefaultInterval is the default interval, in seconds
50
	DefaultInterval = 1
51
)
52

53
// K8sClient represents a Kubernetes client
54
type K8sClient interface {
55
	GetCoreClient(string) (typed_core.CoreV1Interface, error)
56 57
}

58
// K8sClientGetter can get a K8sClient
59 60
type K8sClientGetter struct{}

61
// K8s is the current K8sClient
62
var K8s K8sClient
63 64

func init() {
65
	K8s = &K8sClientGetter{}
66 67
}

68
// GetCoreClient returns a core client
69 70
func (k *K8sClientGetter) GetCoreClient(context string) (typed_core.CoreV1Interface, error) {
	client, err := kapi.Client(context)
71
	if err != nil {
72
		return nil, errors.Wrap(err, "client")
73
	}
T
tstromberg 已提交
74
	return client.CoreV1(), nil
75 76
}

77 78 79 80
// SvcURL represents a service URL. Each item in the URLs field combines the service URL with one of the configured
// node ports. The PortNames field contains the configured names of the ports in the URLs field (sorted correspondingly -
// first item in PortNames belongs to the first item in URLs).
type SvcURL struct {
81 82 83
	Namespace string
	Name      string
	URLs      []string
84
	PortNames []string
85 86
}

87
// URLs represents a list of URL
88
type URLs []SvcURL
89

90
// GetServiceURLs returns a SvcURL object for every service in a particular namespace.
A
AdamDang 已提交
91
// Accepts a template for formatting
92
func GetServiceURLs(api libmachine.API, cname string, namespace string, t *template.Template) (URLs, error) {
93
	host, err := machine.LoadHost(api, cname)
94 95 96 97 98 99 100 101 102
	if err != nil {
		return nil, err
	}

	ip, err := host.Driver.GetIP()
	if err != nil {
		return nil, err
	}

103
	client, err := K8s.GetCoreClient(cname)
104 105 106 107
	if err != nil {
		return nil, err
	}

108
	serviceInterface := client.Services(namespace)
109

P
Predrag Rogic 已提交
110
	svcs, err := serviceInterface.List(context.Background(), meta.ListOptions{})
111 112 113 114
	if err != nil {
		return nil, err
	}

115
	var serviceURLs []SvcURL
116
	for _, svc := range svcs.Items {
117
		svcURL, err := printURLsForService(client, ip, svc.Name, svc.Namespace, t)
118 119 120
		if err != nil {
			return nil, err
		}
121
		serviceURLs = append(serviceURLs, svcURL)
122 123 124 125 126
	}

	return serviceURLs, nil
}

127
// GetServiceURLsForService returns a SvcURL object for a service in a namespace. Supports optional formatting.
128
func GetServiceURLsForService(api libmachine.API, cname string, namespace, service string, t *template.Template) (SvcURL, error) {
129
	host, err := machine.LoadHost(api, cname)
130
	if err != nil {
131
		return SvcURL{}, errors.Wrap(err, "Error checking if api exist and loading it")
132 133 134 135
	}

	ip, err := host.Driver.GetIP()
	if err != nil {
136
		return SvcURL{}, errors.Wrap(err, "Error getting ip from host")
137 138
	}

139
	client, err := K8s.GetCoreClient(cname)
140
	if err != nil {
141
		return SvcURL{}, err
142 143 144 145 146
	}

	return printURLsForService(client, ip, service, namespace, t)
}

147
func printURLsForService(c typed_core.CoreV1Interface, ip, service, namespace string, t *template.Template) (SvcURL, error) {
148
	if t == nil {
149
		return SvcURL{}, errors.New("Error, attempted to generate service url with nil --format template")
150 151
	}

P
Predrag Rogic 已提交
152
	svc, err := c.Services(namespace).Get(context.Background(), service, meta.GetOptions{})
153
	if err != nil {
154
		return SvcURL{}, errors.Wrapf(err, "service '%s' could not be found running", service)
155
	}
156

P
Predrag Rogic 已提交
157
	endpoints, err := c.Endpoints(namespace).Get(context.Background(), service, meta.GetOptions{})
158
	m := make(map[int32]string)
159
	if err == nil && endpoints != nil && len(endpoints.Subsets) > 0 {
160 161
		for _, ept := range endpoints.Subsets {
			for _, p := range ept.Ports {
T
Thomas Stromberg 已提交
162
				m[p.Port] = p.Name
163 164 165
			}
		}
	}
166

167
	urls := []string{}
168
	portNames := []string{}
169
	for _, port := range svc.Spec.Ports {
170 171

		if port.Name != "" {
172
			m[port.TargetPort.IntVal] = fmt.Sprintf("%s/%d", port.Name, port.Port)
173 174 175 176
		} else {
			m[port.TargetPort.IntVal] = strconv.Itoa(int(port.Port))
		}

177 178 179 180 181 182 183 184 185 186 187 188
		if port.NodePort > 0 {
			var doc bytes.Buffer
			err = t.Execute(&doc, struct {
				IP   string
				Port int32
				Name string
			}{
				ip,
				port.NodePort,
				m[port.TargetPort.IntVal],
			})
			if err != nil {
189
				return SvcURL{}, err
190 191
			}
			urls = append(urls, doc.String())
192
			portNames = append(portNames, m[port.TargetPort.IntVal])
193 194
		}
	}
195
	return SvcURL{Namespace: svc.Namespace, Name: svc.Name, URLs: urls, PortNames: portNames}, nil
196 197
}

198
// CheckService checks if a service is listening on a port.
199
func CheckService(cname string, namespace string, service string) error {
200
	client, err := K8s.GetCoreClient(cname)
201
	if err != nil {
202
		return errors.Wrap(err, "Error getting Kubernetes client")
203
	}
204

P
Predrag Rogic 已提交
205
	svc, err := client.Services(namespace).Get(context.Background(), service, meta.GetOptions{})
206
	if err != nil {
M
Medya Gh 已提交
207
		return &retry.RetriableError{
208
			Err: errors.Wrapf(err, "Error getting service %s", service),
209 210
		}
	}
211 212 213
	if len(svc.Spec.Ports) == 0 {
		return fmt.Errorf("%s:%s has no ports", namespace, service)
	}
214
	klog.Infof("Found service: %+v", svc)
215 216 217
	return nil
}

218
// OptionallyHTTPSFormattedURLString returns a formatted URL string, optionally HTTPS
B
Balint Pato 已提交
219 220 221
func OptionallyHTTPSFormattedURLString(bareURLString string, https bool) (string, bool) {
	httpsFormattedString := bareURLString
	isHTTPSchemedURL := false
222

B
Balint Pato 已提交
223 224
	if u, parseErr := url.Parse(bareURLString); parseErr == nil {
		isHTTPSchemedURL = u.Scheme == "http"
225 226
	}

B
Balint Pato 已提交
227 228
	if isHTTPSchemedURL && https {
		httpsFormattedString = strings.Replace(bareURLString, "http", "https", 1)
229 230
	}

B
Balint Pato 已提交
231
	return httpsFormattedString, isHTTPSchemedURL
232 233
}

234 235 236 237
// PrintServiceList prints a list of services as a table which has
// "Namespace", "Name" and "URL" columns to a writer
func PrintServiceList(writer io.Writer, data [][]string) {
	table := tablewriter.NewWriter(writer)
238
	table.SetHeader([]string{"Namespace", "Name", "Target Port", "URL"})
239 240 241 242 243 244
	table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true})
	table.SetCenterSeparator("|")
	table.AppendBulk(data)
	table.Render()
}

245 246 247 248 249 250 251 252 253 254
// SVCNotFoundError error type handles 'service not found' scenarios
type SVCNotFoundError struct {
	Err error
}

// Error method for SVCNotFoundError type
func (t SVCNotFoundError) Error() string {
	return "Service not found"
}

255
// WaitForService waits for a service, and return the urls when available
256
func WaitForService(api libmachine.API, cname string, namespace string, service string, urlTemplate *template.Template, urlMode bool, https bool,
257 258
	wait int, interval int) ([]string, error) {
	var urlList []string
F
Francis 已提交
259 260 261 262
	// Convert "Amount of time to wait" and "interval of each check" to attempts
	if interval == 0 {
		interval = 1
	}
263

264
	err := CheckService(cname, namespace, service)
265 266 267 268
	if err != nil {
		return nil, &SVCNotFoundError{err}
	}

269
	chkSVC := func() error { return CheckService(cname, namespace, service) }
M
Medya Gh 已提交
270 271

	if err := retry.Expo(chkSVC, time.Duration(interval)*time.Second, time.Duration(wait)*time.Second); err != nil {
272
		return nil, &SVCNotFoundError{err}
273 274
	}

275
	serviceURL, err := GetServiceURLsForService(api, cname, namespace, service, urlTemplate)
276
	if err != nil {
277
		return urlList, errors.Wrap(err, "Check that minikube is running and that you have specified the correct namespace")
278
	}
279 280 281

	if !urlMode {
		var data [][]string
282 283
		if len(serviceURL.URLs) == 0 {
			data = append(data, []string{namespace, service, "", "No node port"})
284
		} else {
285
			data = append(data, []string{namespace, service, strings.Join(serviceURL.PortNames, "\n"), strings.Join(serviceURL.URLs, "\n")})
286 287 288 289
		}
		PrintServiceList(os.Stdout, data)
	}

290
	if len(serviceURL.URLs) == 0 {
291
		out.Styled(style.Sad, "service {{.namespace_name}}/{{.service_name}} has no node port", out.V{"namespace_name": namespace, "service_name": service})
292
		return urlList, nil
293
	}
294

295
	for _, bareURLString := range serviceURL.URLs {
296 297
		url, _ := OptionallyHTTPSFormattedURLString(bareURLString, https)
		urlList = append(urlList, url)
298
	}
299
	return urlList, nil
300 301
}

302
// GetServiceListByLabel returns a ServiceList by label
303
func GetServiceListByLabel(cname string, namespace string, key string, value string) (*core.ServiceList, error) {
304
	client, err := K8s.GetCoreClient(cname)
305
	if err != nil {
M
Medya Gh 已提交
306
		return &core.ServiceList{}, &retry.RetriableError{Err: err}
307
	}
M
Marcin Niemira 已提交
308
	return getServiceListFromServicesByLabel(client.Services(namespace), key, value)
309 310
}

311
func getServiceListFromServicesByLabel(services typed_core.ServiceInterface, key string, value string) (*core.ServiceList, error) {
312
	selector := labels.SelectorFromSet(labels.Set(map[string]string{key: value}))
P
Predrag Rogic 已提交
313
	serviceList, err := services.List(context.Background(), meta.ListOptions{LabelSelector: selector.String()})
314
	if err != nil {
M
Medya Gh 已提交
315
		return &core.ServiceList{}, &retry.RetriableError{Err: err}
316 317 318 319
	}

	return serviceList, nil
}
320 321

// CreateSecret creates or modifies secrets
322
func CreateSecret(cname string, namespace, name string, dataValues map[string]string, labels map[string]string) error {
323
	client, err := K8s.GetCoreClient(cname)
324
	if err != nil {
M
Medya Gh 已提交
325
		return &retry.RetriableError{Err: err}
326
	}
327

328
	secrets := client.Secrets(namespace)
P
Predrag Rogic 已提交
329
	secret, err := secrets.Get(context.Background(), name, meta.GetOptions{})
330
	if err != nil {
331
		klog.Infof("Failed to retrieve existing secret: %v", err)
332
	}
M
Marcin Niemira 已提交
333

334 335
	// Delete existing secret
	if len(secret.Name) > 0 {
336
		err = DeleteSecret(cname, namespace, name)
337
		if err != nil {
M
Medya Gh 已提交
338
			return &retry.RetriableError{Err: err}
339 340 341 342 343 344 345 346 347 348
		}
	}

	// convert strings to data secrets
	data := map[string][]byte{}
	for key, value := range dataValues {
		data[key] = []byte(value)
	}

	// Create Secret
349
	secretObj := &core.Secret{
350
		ObjectMeta: meta.ObjectMeta{
351 352 353 354
			Name:   name,
			Labels: labels,
		},
		Data: data,
355
		Type: core.SecretTypeOpaque,
356 357
	}

P
Predrag Rogic 已提交
358
	_, err = secrets.Create(context.Background(), secretObj, meta.CreateOptions{})
359
	if err != nil {
M
Medya Gh 已提交
360
		return &retry.RetriableError{Err: err}
361 362 363 364 365 366
	}

	return nil
}

// DeleteSecret deletes a secret from a namespace
367
func DeleteSecret(cname string, namespace, name string) error {
368
	client, err := K8s.GetCoreClient(cname)
369
	if err != nil {
M
Medya Gh 已提交
370
		return &retry.RetriableError{Err: err}
371 372 373
	}

	secrets := client.Secrets(namespace)
P
Predrag Rogic 已提交
374
	err = secrets.Delete(context.Background(), name, meta.DeleteOptions{})
375
	if err != nil {
M
Medya Gh 已提交
376
		return &retry.RetriableError{Err: err}
377
	}
M
Marcin Niemira 已提交
378

379 380
	return nil
}