mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
YAML Templates in Create Resource dock tab
Signed-off-by: Pavel Ashevskiy <pavel.ashevskiy@ifellow.ru>
This commit is contained in:
parent
b852384e85
commit
f74d365ee4
@ -1,2 +1,25 @@
|
||||
.CreateResource {
|
||||
}
|
||||
.Select {
|
||||
.Select__single-value {
|
||||
width: 100%
|
||||
}
|
||||
&.TemplateSelect {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.select-template-item{
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
|
||||
.select-template-badge {
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.select-template-group {
|
||||
span {
|
||||
font-size: 13px
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,99 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import os from "os"
|
||||
import chokidar from "chokidar"
|
||||
import filehound from "filehound"
|
||||
import { autobind } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
import { GroupSelectOption, SelectOption} from "../select";
|
||||
|
||||
export interface BadgedSelectOption extends SelectOption {
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
export interface BadgedGroupSelectOption extends GroupSelectOption<BadgedSelectOption> {
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
|
||||
@autobind()
|
||||
export class CreateResourceStore extends DockTabStore<string> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
storageKey: "create_resource"
|
||||
});
|
||||
}
|
||||
|
||||
get lensTemplatesFolder():string {
|
||||
return path.resolve(__static, "../templates/create-resource");
|
||||
}
|
||||
|
||||
get userTemplatesFolder():string {
|
||||
return path.join(os.homedir(), ".k8slens", "templates");
|
||||
}
|
||||
|
||||
|
||||
async getTemplates(templatesPath: string):Promise<GroupSelectOption[]>{
|
||||
const ungroupedTemplates:SelectOption[] = [];
|
||||
const templates:GroupSelectOption[] = [];
|
||||
let groups = await fs.readdir(templatesPath);
|
||||
await Promise.all(groups.map(async (group) =>{
|
||||
const groupAbsPath = path.join(templatesPath, group);
|
||||
if((await fs.stat(groupAbsPath)).isDirectory()){
|
||||
const files = await fs.readdir(groupAbsPath);
|
||||
const templateSelectOptions = await Promise.all(files.map(
|
||||
(template) => ({
|
||||
value: path.join(groupAbsPath,template),
|
||||
label: path.parse(template).name
|
||||
})
|
||||
)
|
||||
);
|
||||
templates.push({label: group.replace("-", " "), options: templateSelectOptions});
|
||||
} else {
|
||||
ungroupedTemplates.push({ value: groupAbsPath, label: path.parse(group).name});
|
||||
}
|
||||
}))
|
||||
if (ungroupedTemplates.length > 0){
|
||||
templates.push({label: "ungrouped", options: ungroupedTemplates})
|
||||
}
|
||||
return templates;
|
||||
}
|
||||
|
||||
async getMergedTemplates():Promise<BadgedGroupSelectOption[]> {
|
||||
const lensTemplates = await this.getTemplates(this.lensTemplatesFolder);
|
||||
const userTemplates = await this.getTemplates(this.userTemplatesFolder);
|
||||
const userGroupOptions:BadgedGroupSelectOption[] = userTemplates.filter(
|
||||
({label}) => !lensTemplates.map(({label}) => label.toString().toLowerCase()).includes(label.toString().toLowerCase())
|
||||
).map(({label,options}) => ({label, badge:"user", options}));
|
||||
// merge lens and user templates within lens groups, then merge lens and user groups
|
||||
const badgedTemplates:BadgedGroupSelectOption[] = lensTemplates.map( ({label, options}) => {
|
||||
const userOptions = userTemplates.find(userTemplate => userTemplate.label.toString().toLowerCase() == label.toString().toLowerCase())
|
||||
return {
|
||||
label,
|
||||
options: userOptions ? options.concat(userOptions.options.map((userOption) => ({label: userOption.label, badge: "user", value: userOption.value}))) : options
|
||||
};
|
||||
})
|
||||
return this.orderTemplatesGroups(userGroupOptions).concat(this.orderTemplatesGroups(badgedTemplates));
|
||||
}
|
||||
|
||||
async watchUserTemplates(calback: ()=> void){
|
||||
|
||||
chokidar.watch(this.userTemplatesFolder).on('all', () => {
|
||||
calback();
|
||||
});
|
||||
}
|
||||
|
||||
orderTemplatesGroups(templates:BadgedGroupSelectOption[]): BadgedGroupSelectOption[] {
|
||||
const compare = (a:BadgedGroupSelectOption|BadgedSelectOption, b:BadgedGroupSelectOption|BadgedSelectOption) => {
|
||||
return a.label.toString() > b.label.toString() ? 1 : a.label.toString() < b.label.toString() ? -1 : 0
|
||||
}
|
||||
return templates.map( group => {
|
||||
group.options = group.options.sort((a, b) => compare(a,b));
|
||||
return group;
|
||||
}).sort((a, b) => compare(a,b));
|
||||
}
|
||||
}
|
||||
|
||||
export const createResourceStore = new CreateResourceStore();
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import "./create-resource.scss";
|
||||
|
||||
import React from "react";
|
||||
import fs from "fs-extra";
|
||||
import {Select, GroupSelectOption, SelectOption} from "../select";
|
||||
import jsYaml from "js-yaml";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { createResourceStore } from "./create-resource.store";
|
||||
import { createResourceStore, BadgedGroupSelectOption } from "./create-resource.store";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { resourceApplierApi } from "../../api/endpoints/resource-applier.api";
|
||||
import { JsonApiErrorParsed } from "../../api/json-api";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Badge } from "../badge";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -21,6 +24,31 @@ interface Props {
|
||||
@observer
|
||||
export class CreateResource extends React.Component<Props> {
|
||||
@observable error = "";
|
||||
@observable templates:GroupSelectOption<SelectOption>[] = [];
|
||||
|
||||
async componentDidMount(){
|
||||
createResourceStore.watchUserTemplates(()=> {
|
||||
this.templates = [];
|
||||
createResourceStore.getMergedTemplates().then(
|
||||
badgedTemplates => {
|
||||
badgedTemplates.map((group) => {
|
||||
this.templates.push(this.convertBadgedGroup(group));
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
convertBadgedGroup(badgedGroup: BadgedGroupSelectOption):GroupSelectOption {
|
||||
const options = badgedGroup.options.map(({label, badge, value }) => ({
|
||||
label: this.renderSelectOption(label.toString(), badge),
|
||||
value,
|
||||
}));
|
||||
return ({
|
||||
label: this.renderSelectGroup(badgedGroup.label.toString(), badgedGroup.badge),
|
||||
options
|
||||
});
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
@ -35,6 +63,10 @@ export class CreateResource extends React.Component<Props> {
|
||||
this.error = error;
|
||||
};
|
||||
|
||||
onSelectTemplate = (item: SelectOption) => {
|
||||
fs.readFile(item.value,"utf8").then(v => createResourceStore.setData(this.tabId,v));
|
||||
};
|
||||
|
||||
create = async () => {
|
||||
if (this.error) return;
|
||||
if (!this.data.trim()) return; // do not save when field is empty
|
||||
@ -67,6 +99,40 @@ export class CreateResource extends React.Component<Props> {
|
||||
return successMessage;
|
||||
};
|
||||
|
||||
renderSelectOption(label: string, badge?: string){
|
||||
return this.renderSelectItem(label,"select-template-option", badge );
|
||||
}
|
||||
|
||||
renderSelectGroup(label: string, badge?: string){
|
||||
return this.renderSelectItem(label,"select-template-group", badge );
|
||||
}
|
||||
|
||||
renderSelectItem(label: string, className: string, badge?: string) {
|
||||
return(
|
||||
<div className={cssNames("flex select-template-item", className)}>
|
||||
<span>{label}</span>
|
||||
{badge && <Badge className="select-template-badge" label={badge} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderControls(){
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Select
|
||||
autoConvertOptions = {false}
|
||||
className="TemplateSelect"
|
||||
placeholder="Select Template ..."
|
||||
options={this.templates}
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
onChange={v => this.onSelectTemplate(v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { tabId, data, error, create, onChange } = this;
|
||||
const { className } = this.props;
|
||||
@ -76,6 +142,7 @@ export class CreateResource extends React.Component<Props> {
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
controls={this.renderControls()}
|
||||
submit={create}
|
||||
submitLabel="Create"
|
||||
showNotifications={false}
|
||||
|
||||
12
templates/create-resource/access-control/ClusterRole.yaml
Normal file
12
templates/create-resource/access-control/ClusterRole.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
# "namespace" omitted since ClusterRoles are not namespaced
|
||||
name: secret-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
#
|
||||
# at the HTTP level, the name of the resource for accessing Secret
|
||||
# objects is "secrets"
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
@ -0,0 +1,13 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
# This cluster role binding allows anyone in the "manager" group to read secrets in any namespace.
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: read-secrets-global
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: manager # Name is case sensitive
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: secret-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
9
templates/create-resource/access-control/Role.yaml
Normal file
9
templates/create-resource/access-control/Role.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
namespace: default
|
||||
name: pod-reader
|
||||
rules:
|
||||
- apiGroups: [""] # "" indicates the core API group
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
17
templates/create-resource/access-control/RoleBinding.yaml
Normal file
17
templates/create-resource/access-control/RoleBinding.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
# This role binding allows "jane" to read pods in the "default" namespace.
|
||||
# You need to already have a Role named "pod-reader" in that namespace.
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: read-pods
|
||||
namespace: default
|
||||
subjects:
|
||||
# You can specify more than one "subject"
|
||||
- kind: User
|
||||
name: jane # "name" is case sensitive
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
# "roleRef" specifies the binding to a Role / ClusterRole
|
||||
kind: Role #this must be Role or ClusterRole
|
||||
name: pod-reader # this must match the name of the Role or ClusterRole you wish to bind to
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
17
templates/create-resource/configuration/ConfigMap.yaml
Normal file
17
templates/create-resource/configuration/ConfigMap.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: game-demo
|
||||
data:
|
||||
# property-like keys; each key maps to a simple value
|
||||
player_initial_lives: "3"
|
||||
ui_properties_file_name: "user-interface.properties"
|
||||
|
||||
# file-like keys
|
||||
game.properties: |
|
||||
enemy.types=aliens,monsters
|
||||
player.maximum-lives=5
|
||||
user-interface.properties: |
|
||||
color.good=purple
|
||||
color.bad=yellow
|
||||
allow.textmode=true
|
||||
8
templates/create-resource/configuration/Secret.yaml
Normal file
8
templates/create-resource/configuration/Secret.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: secret-basic-auth
|
||||
type: kubernetes.io/basic-auth
|
||||
stringData:
|
||||
username: admin
|
||||
password: t0p-Secret
|
||||
19
templates/create-resource/k8s-workloads/CronJob.yaml
Normal file
19
templates/create-resource/k8s-workloads/CronJob.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: hello
|
||||
spec:
|
||||
schedule: "*/1 * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: hello
|
||||
image: busybox
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- date; echo Hello from the Kubernetes cluster
|
||||
restartPolicy: OnFailure
|
||||
44
templates/create-resource/k8s-workloads/DeamonSet.yaml
Normal file
44
templates/create-resource/k8s-workloads/DeamonSet.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: fluentd-elasticsearch
|
||||
namespace: kube-system
|
||||
labels:
|
||||
k8s-app: fluentd-logging
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: fluentd-elasticsearch
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: fluentd-elasticsearch
|
||||
spec:
|
||||
tolerations:
|
||||
# this toleration is to have the daemonset runnable on master nodes
|
||||
# remove it if your masters can't run pods
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: fluentd-elasticsearch
|
||||
image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
|
||||
resources:
|
||||
limits:
|
||||
memory: 200Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 200Mi
|
||||
volumeMounts:
|
||||
- name: varlog
|
||||
mountPath: /var/log
|
||||
- name: varlibdockercontainers
|
||||
mountPath: /var/lib/docker/containers
|
||||
readOnly: true
|
||||
terminationGracePeriodSeconds: 30
|
||||
volumes:
|
||||
- name: varlog
|
||||
hostPath:
|
||||
path: /var/log
|
||||
- name: varlibdockercontainers
|
||||
hostPath:
|
||||
path: /var/lib/docker/containers
|
||||
21
templates/create-resource/k8s-workloads/Deployment.yaml
Normal file
21
templates/create-resource/k8s-workloads/Deployment.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 80
|
||||
13
templates/create-resource/k8s-workloads/Job.yaml
Normal file
13
templates/create-resource/k8s-workloads/Job.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: pi
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: pi
|
||||
image: perl
|
||||
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
|
||||
restartPolicy: Never
|
||||
backoffLimit: 4
|
||||
21
templates/create-resource/k8s-workloads/ReplicaSet.yaml
Normal file
21
templates/create-resource/k8s-workloads/ReplicaSet.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
metadata:
|
||||
name: frontend
|
||||
labels:
|
||||
app: guestbook
|
||||
tier: frontend
|
||||
spec:
|
||||
# modify replicas according to your case
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
tier: frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: php-redis
|
||||
image: gcr.io/google_samples/gb-frontend:v3
|
||||
@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
34
templates/create-resource/k8s-workloads/StatefulSet.yaml
Normal file
34
templates/create-resource/k8s-workloads/StatefulSet.yaml
Normal file
@ -0,0 +1,34 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: web
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx # has to match .spec.template.metadata.labels
|
||||
serviceName: "nginx"
|
||||
replicas: 3 # by default is 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx # has to match .spec.selector.matchLabels
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: nginx
|
||||
image: k8s.gcr.io/nginx-slim:0.8
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: web
|
||||
volumeMounts:
|
||||
- name: www
|
||||
mountPath: /usr/share/nginx/html
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: www
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
storageClassName: "my-storage-class"
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
17
templates/create-resource/networking/Ingress.yaml
Normal file
17
templates/create-resource/networking/Ingress.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: minimal-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /testpath
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: test
|
||||
port:
|
||||
number: 80
|
||||
34
templates/create-resource/networking/NetworkPolicy.yaml
Normal file
34
templates/create-resource/networking/NetworkPolicy.yaml
Normal file
@ -0,0 +1,34 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: test-network-policy
|
||||
namespace: default
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
role: db
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 172.17.0.0/16
|
||||
except:
|
||||
- 172.17.1.0/24
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
project: myproject
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
role: frontend
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
egress:
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.0.0.0/24
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5978
|
||||
11
templates/create-resource/networking/Service.yaml
Normal file
11
templates/create-resource/networking/Service.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: my-service
|
||||
spec:
|
||||
selector:
|
||||
app: MyApp
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 9376
|
||||
18
templates/create-resource/storage/PersistentVolume.yaml
Normal file
18
templates/create-resource/storage/PersistentVolume.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: pv0003
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Recycle
|
||||
storageClassName: slow
|
||||
mountOptions:
|
||||
- hard
|
||||
- nfsvers=4.1
|
||||
nfs:
|
||||
path: /tmp
|
||||
server: 172.17.0.2
|
||||
17
templates/create-resource/storage/PersistentVolumeClaim.yaml
Normal file
17
templates/create-resource/storage/PersistentVolumeClaim.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: myclaim
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
volumeMode: Filesystem
|
||||
resources:
|
||||
requests:
|
||||
storage: 8Gi
|
||||
storageClassName: slow
|
||||
selector:
|
||||
matchLabels:
|
||||
release: "stable"
|
||||
matchExpressions:
|
||||
- {key: environment, operator: In, values: [dev]}
|
||||
Loading…
Reference in New Issue
Block a user