diff --git a/cli/cmd/metrics_test.go b/cli/cmd/metrics_test.go new file mode 100644 index 0000000000000..d28cda6877fba --- /dev/null +++ b/cli/cmd/metrics_test.go @@ -0,0 +1,130 @@ +package cmd + +import ( + "context" + "testing" + + "github.com/linkerd/linkerd2/pkg/k8s" +) + +func TestGetPodsFor(t *testing.T) { + + configs := []string{ + // pod-1 + `apiVersion: v1 +kind: Pod +metadata: + name: pod-1 + namespace: ns + uid: pod-1 + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs-1 + uid: rs-1 +`, + // rs-1 + `apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: rs-1 + namespace: ns + uid: rs-1 + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: Deployment + name: deploy-1 + uid: deploy-1 +spec: + selector: + matchLabels: + app: foo +`, + // deploy-1 + `apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy-1 + namespace: ns + uid: deploy-1 +spec: + selector: + matchLabels: + app: foo +`, + // pod-2 + `apiVersion: v1 +kind: Pod +metadata: + name: pod-2 + namespace: ns + uid: pod-2 + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs-2 + uid: rs-2 +`, + // rs-2 + `apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: rs-2 + namespace: ns + uid: rs-2 + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: Deployment + name: deploy-2 + uid: deploy-2 +spec: + selector: + matchLabels: + app: foo +`, + // deploy-2 + `apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy-2 + namespace: ns + uid: deploy-2 +spec: + selector: + matchLabels: + app: foo +`} + + k8sClient, err := k8s.NewFakeAPI(configs...) + if err != nil { + t.Fatalf("Unexpected error %s", err) + } + + // Both pod-1 and pod-2 have labels which match deploy-1's selector. + // However, only pod-1 is owned by deploy-1 according to the owner references. + // Owner references should be considered authoritative to resolve ambiguity + // when deployments have overlapping seletors. + pods, err := getPodsFor(context.Background(), k8sClient, "ns", "deploy/deploy-1") + if err != nil { + t.Fatalf("Unexpected error %s", err) + } + + if len(pods) != 1 { + for _, p := range pods { + t.Logf("%s/%s", p.Namespace, p.Name) + } + t.Fatalf("Expected 1 pod, got %d", len(pods)) + } +} diff --git a/cli/cmd/repair.go b/cli/cmd/repair.go index 932a6918ffee9..4af45f2228f17 100644 --- a/cli/cmd/repair.go +++ b/cli/cmd/repair.go @@ -97,7 +97,7 @@ func repair(ctx context.Context, forced bool) error { } // Load the stored config - config, err := k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, "linkerd-config", metav1.GetOptions{}) + config, err := k8sAPI.CoreV1().ConfigMaps(controlPlaneNamespace).Get(ctx, k8s.ConfigConfigMapName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("Failed to load linkerd-config: %s", err) } diff --git a/multicluster/charts/linkerd-multicluster-link/README.md b/multicluster/charts/linkerd-multicluster-link/README.md index d215f57b08ddb..c674e70daaecb 100644 --- a/multicluster/charts/linkerd-multicluster-link/README.md +++ b/multicluster/charts/linkerd-multicluster-link/README.md @@ -27,6 +27,7 @@ Kubernetes: `>=1.16.0-0` |-----|------|---------|-------------| | controllerImage | string | `"cr.l5d.io/linkerd/controller"` | Docker image for the Service mirror component (uses the Linkerd controller image) | | controllerImageVersion | string | `"linkerdVersionValue"` | Tag for the Service Mirror container Docker image | +| enableHeadlessServices | bool | `false` | Toggle support for mirroring headless services | | gateway.probe.port | int | `4191` | The port used for liveliness probing | | logLevel | string | `"info"` | Log level for the Multicluster components | | serviceMirrorRetryLimit | int | `3` | Number of times update from the remote cluster is allowed to be requeued (retried) | diff --git a/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml b/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml index 808855398c051..6569f00b7346f 100644 --- a/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml +++ b/multicluster/charts/linkerd-multicluster-link/templates/service-mirror.yaml @@ -101,6 +101,9 @@ spec: - -log-level={{.Values.logLevel}} - -event-requeue-limit={{.Values.serviceMirrorRetryLimit}} - -namespace={{.Release.Namespace}} + {{- if .Values.enableHeadlessServices }} + - -enable-headless-services + {{- end }} - {{.Values.targetClusterName}} image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion}} name: service-mirror diff --git a/multicluster/charts/linkerd-multicluster-link/values.yaml b/multicluster/charts/linkerd-multicluster-link/values.yaml index 45e8b7d325573..a0879f6d2e053 100644 --- a/multicluster/charts/linkerd-multicluster-link/values.yaml +++ b/multicluster/charts/linkerd-multicluster-link/values.yaml @@ -3,6 +3,8 @@ controllerImage: cr.l5d.io/linkerd/controller # -- Tag for the Service Mirror container Docker image controllerImageVersion: linkerdVersionValue +# -- Toggle support for mirroring headless services +enableHeadlessServices: false gateway: probe: # -- The port used for liveliness probing diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go index 09d14ed00fb84..0d0e9024fa48f 100644 --- a/pkg/k8s/labels.go +++ b/pkg/k8s/labels.go @@ -259,8 +259,8 @@ const ( // ConfigConfigMapName is the name of the ConfigMap containing the linkerd controller configuration. ConfigConfigMapName = "linkerd-config" - // AddOnsConfigMapName is the name of the ConfigMap containing the linkerd add-ons configuration. - AddOnsConfigMapName = "linkerd-config-addons" + // DebugContainerName is the name of the default linkerd debug container + DebugContainerName = "linkerd-debug" // DebugSidecarImage is the image name of the default linkerd debug container DebugSidecarImage = "cr.l5d.io/linkerd/debug" @@ -294,9 +294,6 @@ const ( // IdentityIssuerTrustAnchorsNameExternal is the issuer's certificate file (when using cert-manager). IdentityIssuerTrustAnchorsNameExternal = "ca.crt" - // IdentityIssuerTrustAnchorsName is the trust anchors name. - IdentityIssuerTrustAnchorsName = "ca-bundle.crt" - // ProxyPortName is the name of the Linkerd Proxy's proxy port. ProxyPortName = "linkerd-proxy" diff --git a/pkg/k8s/portforward.go b/pkg/k8s/portforward.go index 7dea912f2e4d9..554a227b47924 100644 --- a/pkg/k8s/portforward.go +++ b/pkg/k8s/portforward.go @@ -124,7 +124,17 @@ func newPortForward( emitLogs bool, ) (*PortForward, error) { - req := k8sAPI.CoreV1().RESTClient().Post(). + restClient := k8sAPI.CoreV1().RESTClient() + // This early return is for testing purposes. If the k8sAPI is a fake + // client, attempting to create a request will result in a nil-pointer + // panic. Instead, we return with no port-forward and no error. + if fakeRest, ok := restClient.(*rest.RESTClient); ok { + if fakeRest == nil { + return nil, nil + } + } + + req := restClient.Post(). Resource("pods"). Namespace(namespace). Name(podName). diff --git a/pkg/k8s/portforward_test.go b/pkg/k8s/portforward_test.go index 989070e811a73..e1a169b98457f 100644 --- a/pkg/k8s/portforward_test.go +++ b/pkg/k8s/portforward_test.go @@ -69,57 +69,177 @@ spec: } func TestNewPortForward(t *testing.T) { - // TODO: test successful cases by mocking out `clientset.CoreV1().RESTClient()` tests := []struct { - ns string - deployName string - k8sConfigs []string - err error + description string + ns string + deployName string + k8sConfigs []string + err error }{ { - "pod-ns", - "deploy-name", + "Pod is owned by the specified deployment", + "ns", + "deploy", []string{`apiVersion: v1 kind: Pod metadata: - name: bad-name - namespace: pod-ns + name: pod + namespace: ns + uid: pod + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: Deployment + name: rs + uid: rs status: phase: Running`, - }, - errors.New("no running pods found for deploy-name"), + `apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: rs + namespace: ns + uid: rs + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: Deployment + name: deploy + uid: deploy + spec: + selector: + matchLabels: + app: foo +`, + `apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy + namespace: ns + uid: deploy +spec: + selector: + matchLabels: + app: foo +`}, + nil, }, + // In the case of overlapping deployments, a pod may match the label + // selector of more than one deployment but will still be owned by + // exactly one. { - "pod-ns", - "deploy-name", + "Pod's labels match, but is not owned by the deployment", + "ns", + "deploy", []string{`apiVersion: v1 kind: Pod metadata: - name: deploy-name-foo-bar - namespace: bad-ns -status: - phase: Running`, - }, - errors.New("no running pods found for deploy-name"), + name: pod + namespace: ns + uid: pod + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs + uid: SOME-OTHER-UID + status: + phase: Running`, + `apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: rs + namespace: ns + uid: rs + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: Deployment + name: deploy + uid: deploy + spec: + selector: + matchLabels: + app: foo +`, + `apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy + namespace: ns + uid: deploy +spec: + selector: + matchLabels: + app: foo +`}, + errors.New("no running pods found for deploy"), }, { - "pod-ns", - "deploy-name", + "Pod is owned by the specified deployment but is not running", + "ns", + "deploy", []string{`apiVersion: v1 kind: Pod metadata: - name: deploy-name-foo-bar - namespace: pod-ns -status: - phase: Stopped`, - }, - errors.New("no running pods found for deploy-name"), + name: pod + namespace: ns + uid: pod + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs + uid: rs + status: + phase: Stopped`, + `apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: rs + namespace: ns + uid: rs + labels: + app: foo + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: Deployment + name: deploy + uid: deploy +spec: + selector: + matchLabels: + app: foo +`, + `apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy + namespace: ns + uid: deploy +spec: + selector: + matchLabels: + app: foo +`}, + errors.New("no running pods found for deploy"), }, } - for i, test := range tests { + for _, test := range tests { test := test // pin - t.Run(fmt.Sprintf("%d: NewPortForward returns expected result", i), func(t *testing.T) { + t.Run(test.description, func(t *testing.T) { k8sClient, err := NewFakeAPI(test.k8sConfigs...) if err != nil { t.Fatalf("Unexpected error %s", err) diff --git a/policy-controller/Cargo.lock b/policy-controller/Cargo.lock index baff59b1ca82d..699a56134e919 100644 --- a/policy-controller/Cargo.lock +++ b/policy-controller/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" +checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" [[package]] name = "async-stream" diff --git a/testutil/inject_validator.go b/testutil/inject_validator.go index 0fa7c0ee666a3..270c47c52fc9b 100644 --- a/testutil/inject_validator.go +++ b/testutil/inject_validator.go @@ -12,9 +12,6 @@ import ( ) const enabled = "true" -const proxyContainerName = "linkerd-proxy" -const initContainerName = "linkerd-init" -const debugContainerName = "linkerd-debug" // InjectValidator is used as a helper to generate // correct injector flags and annotations and verify @@ -91,18 +88,18 @@ func (iv *InjectValidator) validatePort(container *v1.Container, portName string func (iv *InjectValidator) validateDebugContainer(pod *v1.PodSpec) error { if iv.EnableDebug { - proxyContainer := iv.getContainer(pod, debugContainerName, false) + proxyContainer := iv.getContainer(pod, k8s.DebugContainerName, false) if proxyContainer == nil { - return fmt.Errorf("container %s missing", debugContainerName) + return fmt.Errorf("container %s missing", k8s.DebugContainerName) } } return nil } func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error { - proxyContainer := iv.getContainer(pod, proxyContainerName, false) + proxyContainer := iv.getContainer(pod, k8s.ProxyContainerName, false) if proxyContainer == nil { - return fmt.Errorf("container %s missing", proxyContainerName) + return fmt.Errorf("container %s missing", k8s.ProxyContainerName) } if iv.AdminPort != 0 { @@ -116,7 +113,7 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error { return fmt.Errorf("readinessProbe: expected: %d, actual %d", iv.AdminPort, proxyContainer.LivenessProbe.HTTPGet.Port.IntVal) } - if err := iv.validatePort(proxyContainer, "linkerd-admin", iv.AdminPort); err != nil { + if err := iv.validatePort(proxyContainer, k8s.ProxyAdminPortName, iv.AdminPort); err != nil { return err } } @@ -149,7 +146,7 @@ func (iv *InjectValidator) validateProxyContainer(pod *v1.PodSpec) error { if proxyContainer.ReadinessProbe.HTTPGet.Port.IntVal != int32(iv.AdminPort) { return fmt.Errorf("readinessProbe: expected: %d, actual %d", iv.AdminPort, proxyContainer.LivenessProbe.HTTPGet.Port.IntVal) } - if err := iv.validatePort(proxyContainer, "linkerd-proxy", iv.InboundPort); err != nil { + if err := iv.validatePort(proxyContainer, k8s.ProxyPortName, iv.InboundPort); err != nil { return err } } @@ -285,9 +282,9 @@ func (iv *InjectValidator) validateInitContainer(pod *v1.PodSpec) error { if iv.NoInitContainer { return nil } - initContainer := iv.getContainer(pod, initContainerName, true) + initContainer := iv.getContainer(pod, k8s.InitContainerName, true) if initContainer == nil { - return fmt.Errorf("container %s missing", initContainerName) + return fmt.Errorf("container %s missing", k8s.InitContainerName) } if iv.InitImage != "" || iv.InitImageVersion != "" { diff --git a/viz/cmd/stat.go b/viz/cmd/stat.go index c00e580c9f21a..f711f7ed3c9d5 100644 --- a/viz/cmd/stat.go +++ b/viz/cmd/stat.go @@ -150,6 +150,18 @@ If no resource name is specified, displays stats about all resources of the spec # Get all pods in all namespaces that call the hello1 service in the test namespace. linkerd viz stat pods --to svc/hello1 --to-namespace test --all-namespaces + # Get the web service. With Services, metrics are generated from the outbound metrics + # of clients, and thus will not include unmeshed client request metrics. + linkerd viz stat svc/web + + # Get the web services and metrics for any traffic coming to the service from the hello1 deployment + # in the test namespace. + linkerd viz stat svc/web --from deploy/hello1 --from-namespace test + + # Get the web services and metrics for all the traffic that reaches the web-pod1 pod + # in the test namespace exclusively. + linkerd viz stat svc/web --to pod/web-pod1 --to-namespace test + # Get all services in all namespaces that receive calls from hello1 deployment in the test namespace. linkerd viz stat services --from deploy/hello1 --from-namespace test --all-namespaces diff --git a/web/app/js/components/ServiceMesh.jsx b/web/app/js/components/ServiceMesh.jsx index 7e09776261623..bc09a578d92c7 100644 --- a/web/app/js/components/ServiceMesh.jsx +++ b/web/app/js/components/ServiceMesh.jsx @@ -34,6 +34,17 @@ const styles = { }, }; +const installedExtensionsColumn = [ + { + title: columnTitleName, + dataIndex: 'name', + }, + { + title: columnTitleNamespace, + dataIndex: 'namespace', + }, +]; + const serviceMeshDetailsColumns = [ { title: columnTitleName, @@ -72,6 +83,7 @@ class ServiceMesh extends React.Component { this.state = { pollingInterval: 2000, components: [], + extensions: [], nsStatuses: [], pendingRequests: false, loaded: false, @@ -81,6 +93,7 @@ class ServiceMesh extends React.Component { componentDidMount() { this.startServerPolling(); + this.fetchAllInstalledExtensions(); } componentDidUpdate(prevProps) { @@ -109,6 +122,12 @@ class ServiceMesh extends React.Component { ]; } + getInstalledExtensions() { + const { extensions } = this.state; + const extensionList = !_isEmpty(extensions.extensions) ? extensions.extensions : []; + return extensionList; + } + getControllerComponentData = podData => { const podDataByDeploy = _groupBy(_filter(podData.pods, d => d.controlPlane), p => p.deployment); const byDeployName = _mapKeys(podDataByDeploy, (_pods, dep) => dep.split('/')[1]); @@ -190,6 +209,15 @@ class ServiceMesh extends React.Component { .catch(this.handleApiError); } + fetchAllInstalledExtensions() { + this.api.setCurrentRequests([this.api.fetchExtension()]); + this.serverPromise = Promise.all(this.api.getCurrentPromises()) + .then(([extensions]) => { + this.setState({ extensions }); + }) + .catch(this.handleApiError); + } + handleApiError(e) { if (e.isCanceled) { return; @@ -234,6 +262,23 @@ class ServiceMesh extends React.Component { ); } + renderInstalledExtensions() { + return ( + + + + Installed Extensions + + + d.uid} /> + + ); + } + renderServiceMeshDetails() { return ( @@ -298,6 +343,7 @@ class ServiceMesh extends React.Component { {this.renderControlPlaneDetails()} + {this.renderInstalledExtensions()} diff --git a/web/app/js/components/util/ApiHelpers.jsx b/web/app/js/components/util/ApiHelpers.jsx index 534761539ac55..58371fb08a78d 100644 --- a/web/app/js/components/util/ApiHelpers.jsx +++ b/web/app/js/components/util/ApiHelpers.jsx @@ -61,7 +61,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => { const servicesPath = '/api/services'; const edgesPath = '/api/edges'; const gatewaysPath = '/api/gateways'; - const l5dExtensionsPath = '/api/extension'; + const l5dExtensionsPath = '/api/extensions'; const validMetricsWindows = { '10s': '10 minutes', @@ -147,7 +147,11 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => { }; const fetchExtension = name => { - return apiFetch(`${l5dExtensionsPath}?extension_name=${name}`); + let extensionPath = l5dExtensionsPath; + if (name) { + extensionPath += `?extension_name=${name}`; + } + return apiFetch(extensionPath); }; const fetchResourceDefinition = (namespace, resourceType, resourceName) => { diff --git a/web/app/package.json b/web/app/package.json index ca761c877aa20..7f0f6c91f9eed 100644 --- a/web/app/package.json +++ b/web/app/package.json @@ -46,7 +46,7 @@ "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/preset-env": "^7.15.0", "@babel/preset-react": "^7.14.5", - "@babel/runtime": "^7.14.8", + "@babel/runtime": "^7.15.3", "@lingui/cli": "3.10.2", "babel-core": "^7.0.0-bridge.0", "babel-jest": "^27.0.6", @@ -68,7 +68,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "eslint-webpack-plugin": "^3.0.1", "file-loader": "^6.2.0", - "history": "5.0.0", + "history": "5.0.1", "html-webpack-plugin": "^5.3.2", "jest": "^27.0.6", "jest-enzyme": "7.1.2", @@ -78,7 +78,7 @@ "sinon-stub-promise": "4.0.0", "style-loader": "^3.2.1", "url-loader": "^4.1.1", - "webpack": "^5.49.0", + "webpack": "^5.50.0", "webpack-bundle-analyzer": "4.4.2", "webpack-cli": "4.7.2", "webpack-dev-server": "3.11.2" diff --git a/web/app/yarn.lock b/web/app/yarn.lock index ba48615cf3fbf..048ed0f2eb012 100644 --- a/web/app/yarn.lock +++ b/web/app/yarn.lock @@ -955,10 +955,10 @@ core-js-pure "^3.15.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" - integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" + integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== dependencies: regenerator-runtime "^0.13.4" @@ -4756,10 +4756,10 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -history@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" - integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg== +history@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/history/-/history-5.0.1.tgz#de35025ed08bce0db62364b47ebbf9d97b5eb06a" + integrity sha512-5qC/tFUKfVci5kzgRxZxN5Mf1CV8NmJx9ByaPX0YTLx5Vz3Svh7NYp6eA4CpDq4iA9D0C1t8BNIfvQIrUI3mVw== dependencies: "@babel/runtime" "^7.7.6" @@ -9252,9 +9252,9 @@ url-loader@^4.1.1: schema-utils "^3.0.0" url-parse@^1.4.3, url-parse@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -9521,10 +9521,10 @@ webpack-sources@^3.2.0: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d" integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw== -webpack@^5.49.0: - version "5.49.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.49.0.tgz#e250362b781a9fb614ba0a97ed67c66b9c5310cd" - integrity sha512-XarsANVf28A7Q3KPxSnX80EkCcuOer5hTOEJWJNvbskOZ+EK3pobHarGHceyUZMxpsTHBHhlV7hiQyLZzGosYw== +webpack@^5.50.0: + version "5.50.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.50.0.tgz#5562d75902a749eb4d75131f5627eac3a3192527" + integrity sha512-hqxI7t/KVygs0WRv/kTgUW8Kl3YC81uyWQSo/7WUs5LsuRw0htH/fCwbVBGCuiX/t4s7qzjXFcf41O8Reiypag== dependencies: "@types/eslint-scope" "^3.7.0" "@types/estree" "^0.0.50" diff --git a/web/srv/api_handlers.go b/web/srv/api_handlers.go index 0e2411de89bdc..5e73fa3e8e58b 100644 --- a/web/srv/api_handlers.go +++ b/web/srv/api_handlers.go @@ -434,23 +434,54 @@ func (h *handler) handleAPIResourceDefinition(w http.ResponseWriter, req *http.R w.Write(resourceDefinition) } -func (h *handler) handleGetExtension(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { +func (h *handler) handleGetExtensions(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { ctx := req.Context() extensionName := req.FormValue("extension_name") + type Extension struct { + Name string `json:"name"` + UID string `json:"uid"` + Namespace string `json:"namespace"` + } + resp := map[string]interface{}{} - ns, err := h.k8sAPI.GetNamespaceWithExtensionLabel(ctx, extensionName) - if err != nil && strings.HasPrefix(err.Error(), "could not find") { + if extensionName != "" { + ns, err := h.k8sAPI.GetNamespaceWithExtensionLabel(ctx, extensionName) + if err != nil && strings.HasPrefix(err.Error(), "could not find") { + renderJSON(w, resp) + return + } else if err != nil { + renderJSONError(w, err, http.StatusInternalServerError) + return + } + + resp["data"] = Extension{ + UID: string(ns.UID), + Name: ns.GetLabels()[k8s.LinkerdExtensionLabel], + Namespace: ns.Name, + } + renderJSON(w, resp) return - } else if err != nil { + } + + installedExtensions, err := h.k8sAPI.GetAllNamespacesWithExtensionLabel(ctx) + if err != nil { renderJSONError(w, err, http.StatusInternalServerError) return } - resp["extensionName"] = ns.GetLabels()[k8s.LinkerdExtensionLabel] - resp["namespace"] = ns.Name + extensionList := make([]Extension, len(installedExtensions)) + + for i, installedExtension := range installedExtensions { + extensionList[i] = Extension{ + UID: string(installedExtension.GetObjectMeta().GetUID()), + Name: installedExtension.GetLabels()[k8s.LinkerdExtensionLabel], + Namespace: installedExtension.GetName(), + } + } + resp["extensions"] = extensionList renderJSON(w, resp) } diff --git a/web/srv/server.go b/web/srv/server.go index 8219b3c626689..2946991606491 100644 --- a/web/srv/server.go +++ b/web/srv/server.go @@ -196,7 +196,7 @@ func NewServer( server.router.GET("/api/check", handler.handleAPICheck) server.router.GET("/api/resource-definition", handler.handleAPIResourceDefinition) server.router.GET("/api/gateways", handler.handleAPIGateways) - server.router.GET("/api/extension", handler.handleGetExtension) + server.router.GET("/api/extensions", handler.handleGetExtensions) // grafana proxy server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)