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)