mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-12-22 10:48:30 +00:00
650 lines
21 KiB
Plaintext
650 lines
21 KiB
Plaintext
local stage = std.extVar('stage');
|
|
local tag = std.extVar('tag');
|
|
local ociRegistry = std.extVar('oci_registry');
|
|
local ociRegistryRepo = std.extVar('oci_registry_repo');
|
|
local ociRegistryDocker = std.extVar('oci_registry_docker');
|
|
local ociNoNestedSupport = std.extVar('oci_registry_no_nested_support_in_2022_shame_on_you_aws') == 'true';
|
|
local site = std.extVar('site');
|
|
local arch = std.extVar('arch');
|
|
local localEnvironment = std.extVar('local_environment') == '1';
|
|
|
|
local stageNoDash = std.strReplace(stage, '-', '');
|
|
|
|
local imagePullPolicy = if stageNoDash == 'dev' then 'Always' else 'IfNotPresent';
|
|
|
|
local utils = import 'ci/utils.jsonnet';
|
|
|
|
local defaultEnvs = [
|
|
{
|
|
name: 'RESF_ENV',
|
|
value: stageNoDash,
|
|
},
|
|
{
|
|
name: 'RESF_NS',
|
|
valueFrom: true,
|
|
field: 'metadata.namespace',
|
|
},
|
|
{
|
|
name: 'RESF_SERVICE_ACCOUNT',
|
|
valueFrom: true,
|
|
field: 'spec.serviceAccountName',
|
|
},
|
|
{
|
|
name: 'AWS_REGION',
|
|
value: 'us-east-2',
|
|
},
|
|
{
|
|
name: 'LOCALSTACK_ENDPOINT',
|
|
value: if utils.local_image then 'http://localstack.default.svc.cluster.local:4566' else '',
|
|
}
|
|
];
|
|
|
|
local define_env(envsOrig) = std.filter(function(x) x != null, [
|
|
if field != null then {
|
|
name: field.name,
|
|
value: if std.objectHas(field, 'value') then field.value,
|
|
valueFrom: if std.objectHas(field, 'valueFrom') && field.valueFrom == true then {
|
|
secretKeyRef: if std.objectHas(field, 'secret') then {
|
|
name: field.secret.name,
|
|
key: field.secret.key,
|
|
},
|
|
fieldRef: if std.objectHas(field, 'field') then {
|
|
fieldPath: field.field,
|
|
},
|
|
},
|
|
}
|
|
for field in (envsOrig + defaultEnvs)
|
|
]);
|
|
|
|
local define_volumes(volumes) = [
|
|
{
|
|
name: vm.name,
|
|
persistentVolumeClaim: if std.objectHas(vm, 'pvc') then {
|
|
claimName: vm.name,
|
|
},
|
|
emptyDir: if std.objectHas(vm, 'emptyDir') then {},
|
|
secret: if std.objectHas(vm, 'secret') then vm.secret,
|
|
configMap: if std.objectHas(vm, 'configMap') then vm.configMap,
|
|
hostPath: if std.objectHas(vm, 'hostPath') then vm.hostPath,
|
|
}
|
|
for vm in volumes
|
|
];
|
|
|
|
local define_volume_mounts(volumes) = [
|
|
{
|
|
name: vm.name,
|
|
mountPath: vm.path,
|
|
}
|
|
for vm in volumes
|
|
];
|
|
|
|
local define_init_containers(initc_) = std.filter(function(x) x != null, [
|
|
if initc != null && std.objectHas(initc, 'name') then {
|
|
name: initc.name,
|
|
image: initc.image,
|
|
imagePullPolicy: imagePullPolicy,
|
|
command: if std.objectHas(initc, 'command') then initc.command,
|
|
args: if std.objectHas(initc, 'args') then initc.args,
|
|
env: define_env(if std.objectHas(initc, 'env') then initc.env else []),
|
|
volumeMounts: if std.objectHas(initc, 'volumes') && initc.volumes != null then define_volume_mounts(initc.volumes),
|
|
}
|
|
for initc in initc_
|
|
]);
|
|
|
|
local default_labels = {
|
|
env: stageNoDash
|
|
};
|
|
|
|
local fix_metadata(metadata) = metadata {
|
|
namespace: metadata.namespace,
|
|
};
|
|
|
|
local prod() = stage == '-prod';
|
|
local dev() = stage == '-dev';
|
|
|
|
{
|
|
// For reference
|
|
metadata: {
|
|
name: 'empty',
|
|
namespace: 'namespace',
|
|
annotations: {},
|
|
},
|
|
|
|
// Namespace
|
|
define_namespace(name, metadata={})::
|
|
{
|
|
apiVersion: 'v1',
|
|
kind: 'Namespace',
|
|
metadata: metadata {
|
|
name: name,
|
|
},
|
|
},
|
|
|
|
// Deployment
|
|
define_deployment(metadataOrig, deporig)::
|
|
local _ = std.assertEqual(true, std.objectHas(deporig, 'image'));
|
|
local _ = std.assertEqual(true, std.objectHas(deporig, 'tag'));
|
|
local metadata = fix_metadata(metadataOrig);
|
|
|
|
local deployment = deporig {
|
|
annotations: if !std.objectHas(deporig, 'annotations') then {} else deporig.annotations,
|
|
labels: if !std.objectHas(deporig, 'labels') then default_labels else deporig.labels + default_labels,
|
|
volumes: if !std.objectHas(deporig, 'volumes') then [] else deporig.volumes,
|
|
imagePulLSecrets: if !std.objectHas(deporig, 'imagePullSecrets') then deporig.imagePullSecrets else deporig.imagePullSecrets,
|
|
env: if !std.objectHas(deporig, 'env') then [] else deporig.env,
|
|
ports: if !std.objectHas(deporig, 'ports') then [{ containerPort: 80, protocol: 'TCP' }] else deporig.ports,
|
|
initContainers: if !std.objectHas(deporig, 'initContainers') then [] else deporig.initContainers,
|
|
limits: if std.objectHas(deporig, 'limits') then deporig.limits,
|
|
requests: if !std.objectHas(deporig, 'requests') || deporig.requests == null then { cpu: '0.001', memory: '128M' } else deporig.requests,
|
|
};
|
|
|
|
{
|
|
apiVersion: 'apps/v1',
|
|
kind: 'Deployment',
|
|
metadata: metadata {
|
|
name: metadata.name + '-deployment',
|
|
},
|
|
spec: {
|
|
revisionHistoryLimit: 15,
|
|
selector: {
|
|
matchLabels: {
|
|
app: metadata.name,
|
|
env: stageNoDash
|
|
},
|
|
},
|
|
replicas: deployment.replicas,
|
|
strategy: {
|
|
type: 'RollingUpdate',
|
|
rollingUpdate: {
|
|
maxSurge: '300%',
|
|
maxUnavailable: '0%',
|
|
},
|
|
},
|
|
template: {
|
|
metadata: {
|
|
annotations: deployment.annotations,
|
|
labels: deployment.labels {
|
|
app: metadata.name,
|
|
env: stageNoDash,
|
|
version: deployment.tag,
|
|
},
|
|
},
|
|
spec: {
|
|
automountServiceAccountToken: true,
|
|
serviceAccountName: if std.objectHas(deployment, 'serviceAccount') then deployment.serviceAccount,
|
|
initContainers: if std.objectHas(deployment, 'initContainers') && deployment.initContainers != null then define_init_containers(deployment.initContainers),
|
|
securityContext: {
|
|
fsGroup: 1000,
|
|
},
|
|
containers: [
|
|
{
|
|
image: deployment.image + (if ociNoNestedSupport then '-' else ':') + deployment.tag,
|
|
imagePullPolicy: imagePullPolicy,
|
|
name: metadata.name,
|
|
command: if std.objectHas(deployment, 'command') then deployment.command else null,
|
|
args: if std.objectHas(deployment, 'args') then deployment.args else null,
|
|
ports: deployment.ports,
|
|
env: define_env(deployment.env),
|
|
volumeMounts: if std.objectHas(deployment, 'volumes') && deployment.volumes != null then define_volume_mounts(deployment.volumes),
|
|
securityContext: {
|
|
runAsGroup: if std.objectHas(deployment, 'fsGroup') then deployment.fsGroup else null,
|
|
runAsUser: if std.objectHas(deployment, 'fsUser') then deployment.fsUser else null,
|
|
},
|
|
resources: {
|
|
limits: deployment.limits,
|
|
requests: deployment.requests,
|
|
},
|
|
readinessProbe: if std.objectHas(deployment, 'health') && deployment.health != null then {
|
|
httpGet: if !std.objectHas(deployment.health, 'grpc') || !deployment.health.grpc then {
|
|
path: if std.objectHas(deployment.health, 'path') then deployment.health.path else '/_/healthz',
|
|
port: deployment.health.port,
|
|
httpHeaders: [
|
|
{
|
|
name: 'byc-internal-req',
|
|
value: 'yes',
|
|
},
|
|
],
|
|
},
|
|
exec: if std.objectHas(deployment.health, 'grpc') && deployment.health.grpc then {
|
|
command: ["grpc_health_probe", "-connect-timeout=4s", "-v", "-addr=localhost:"+deployment.health.port],
|
|
},
|
|
initialDelaySeconds: if std.objectHas(deployment.health, 'initialDelaySeconds') then deployment.health.initialDelaySeconds else 1,
|
|
periodSeconds: if std.objectHas(deployment.health, 'periodSeconds') then deployment.health.periodSeconds else 3,
|
|
timeoutSeconds: if std.objectHas(deployment.health, 'timeoutSeconds') then deployment.health.timeoutSeconds else 5,
|
|
successThreshold: if std.objectHas(deployment.health, 'successThreshold') then deployment.health.successThreshold else 1,
|
|
failureThreshold: if std.objectHas(deployment.health, 'failureTreshold') then deployment.health.failureTreshold else 30,
|
|
} else if std.objectHas(deployment, 'health_tcp') && deployment.health_tcp != null then {
|
|
tcpSocket: {
|
|
port: deployment.health_tcp.port,
|
|
},
|
|
initialDelaySeconds: if std.objectHas(deployment.health, 'initialDelaySeconds') then deployment.health.initialDelaySeconds else 5,
|
|
periodSeconds: if std.objectHas(deployment.health, 'periodSeconds') then deployment.health.periodSeconds else 5,
|
|
},
|
|
},
|
|
],
|
|
affinity: if !std.objectHas(deployment, 'no_anti_affinity') || !deployment.no_anti_affinity then {
|
|
podAntiAffinity: {
|
|
preferredDuringSchedulingIgnoredDuringExecution: [
|
|
{
|
|
weight: 99,
|
|
podAffinityTerm: {
|
|
labelSelector: {
|
|
matchExpressions: [
|
|
{
|
|
key: 'app',
|
|
operator: 'In',
|
|
values: [
|
|
metadata.name,
|
|
],
|
|
},
|
|
],
|
|
},
|
|
topologyKey: 'kubernetes.io/hostname',
|
|
},
|
|
},
|
|
{
|
|
weight: 100,
|
|
podAffinityTerm: {
|
|
labelSelector: {
|
|
matchExpressions: [
|
|
{
|
|
key: 'app',
|
|
operator: 'In',
|
|
values: [
|
|
metadata.name,
|
|
],
|
|
},
|
|
],
|
|
},
|
|
topologyKey: 'failure-domain.beta.kubernetes.io/zone',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
restartPolicy: 'Always',
|
|
imagePullSecrets: if std.objectHas(deployment, 'imagePullSecrets') && deployment.imagePullSecrets != null then [
|
|
{
|
|
name: secret,
|
|
}
|
|
for secret in deployment.imagePullSecrets
|
|
],
|
|
volumes: if std.objectHas(deployment, 'volumes') && deployment.volumes != null then define_volumes(deployment.volumes),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
// Ingress
|
|
define_ingress(metadataOrig, host, srvName=null, path='/', port=80)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
|
|
{
|
|
apiVersion: 'networking.k8s.io/v1',
|
|
kind: 'Ingress',
|
|
metadata: metadata {
|
|
name: metadata.name + '-ingress',
|
|
},
|
|
spec: {
|
|
rules: [{
|
|
host: host,
|
|
http: {
|
|
paths: [
|
|
{
|
|
path: path,
|
|
pathType: 'Prefix',
|
|
backend: {
|
|
service: {
|
|
name: if srvName != null then srvName else metadata.name + '-service',
|
|
port: {
|
|
number: port,
|
|
}
|
|
}
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}],
|
|
} + ({
|
|
tls: [{
|
|
hosts: [
|
|
host,
|
|
],
|
|
secretName: metadata.name + '-tls',
|
|
}],
|
|
}),
|
|
},
|
|
|
|
// Service
|
|
define_service(metadataOrig, externalPort=80, internalPort=80, protocol='TCP', portName='http', selector='', env='canary')::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'v1',
|
|
kind: 'Service',
|
|
metadata: metadata {
|
|
name: metadata.name + '-service',
|
|
},
|
|
spec: {
|
|
type: 'ClusterIP',
|
|
ports: [{
|
|
name: portName,
|
|
port: externalPort,
|
|
protocol: protocol,
|
|
targetPort: internalPort,
|
|
}] + (if portName == 'http' && externalPort != 80 then [{
|
|
name: portName + "-80",
|
|
port: 80,
|
|
protocol: protocol,
|
|
targetPort: internalPort,
|
|
}] else []),
|
|
selector: {
|
|
app: if selector != '' then selector else metadata.name,
|
|
env: env,
|
|
},
|
|
},
|
|
},
|
|
|
|
// Virtual Service
|
|
define_virtual_service(metadataOrig, spec)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'networking.istio.io/v1alpha3',
|
|
kind: 'VirtualService',
|
|
metadata: metadata {
|
|
name: metadata.name + '-vs',
|
|
},
|
|
spec: spec,
|
|
},
|
|
|
|
// Destination rule
|
|
define_destination_rule(metadataOrig, spec)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'networking.istio.io/v1alpha3',
|
|
kind: 'DestinationRule',
|
|
metadata: metadata {
|
|
name: metadata.name + '-dsr',
|
|
},
|
|
spec: spec,
|
|
},
|
|
|
|
// Service entry
|
|
define_service_entry(metadataOrig, hosts, ports, resolution, location)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'networking.istio.io/v1alpha3',
|
|
kind: 'ServiceEntry',
|
|
metadata: metadata {
|
|
name: metadata.name + '-se',
|
|
},
|
|
spec: {
|
|
hosts: hosts,
|
|
ports: ports,
|
|
resolution: resolution,
|
|
location: location,
|
|
},
|
|
},
|
|
|
|
// Job
|
|
define_job(metadataOrig, joborig)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
local job = joborig {
|
|
env: if !std.objectHas(joborig, 'env') then [] else joborig.env,
|
|
labels: if !std.objectHas(joborig, 'labels') then {} else joborig.labels,
|
|
annotations: if !std.objectHas(joborig, 'annotations') then {} else joborig.annotations,
|
|
initContainers: if !std.objectHas(joborig, 'initContainers') then [] else joborig.initContainers,
|
|
volumes: if !std.objectHas(joborig, 'volumes') then [] else joborig.volumes,
|
|
args: if !std.objectHas(joborig, 'args') then [] else joborig.args,
|
|
};
|
|
|
|
local name = metadata.name + '-job';
|
|
|
|
{
|
|
apiVersion: 'batch/v1',
|
|
kind: 'Job',
|
|
metadata: metadata {
|
|
name: name,
|
|
},
|
|
spec: {
|
|
ttlSecondsAfterFinished: 120,
|
|
template: {
|
|
metadata: {
|
|
labels: job.labels,
|
|
annotations: job.annotations,
|
|
},
|
|
spec: {
|
|
automountServiceAccountToken: true,
|
|
serviceAccountName: if std.objectHas(job, 'serviceAccount') then job.serviceAccount,
|
|
imagePullSecrets: if std.objectHas(job, 'imagePullSecrets') && job.imagePullSecrets != null then [
|
|
{
|
|
name: secret,
|
|
}
|
|
for secret in job.imagePullSecrets
|
|
],
|
|
initContainers: define_init_containers(job.initContainers),
|
|
containers: [{
|
|
name: name,
|
|
image: job.image + (if ociNoNestedSupport then '-' else ':') + job.tag,
|
|
command: if std.objectHas(job, 'command') then job.command else null,
|
|
args: job.args,
|
|
env: define_env(job.env),
|
|
volumeMounts: if std.objectHas(job, 'volumes') && job.volumes != null then define_volume_mounts(job.volumes),
|
|
}],
|
|
restartPolicy: 'Never',
|
|
volumes: if std.objectHas(job, 'volumes') && job.volumes != null then define_volumes(job.volumes),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
// ServiceAccount
|
|
define_service_account(metadataOrig)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'v1',
|
|
kind: 'ServiceAccount',
|
|
metadata: metadata {
|
|
name: metadata.name + '-serviceaccount',
|
|
},
|
|
},
|
|
|
|
// Role
|
|
define_role(metadataOrig, rules)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'rbac.authorization.k8s.io/v1',
|
|
kind: 'Role',
|
|
metadata: metadata {
|
|
name: metadata.name + '-role',
|
|
},
|
|
rules: rules,
|
|
},
|
|
|
|
define_role_v2(metadataOrig, name, rules)::
|
|
$.define_role(metadataOrig { name: '%s-%s' % [metadataOrig.name, name] }, rules),
|
|
|
|
// ClusterRole
|
|
define_cluster_role(metadataOrig, rules)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'rbac.authorization.k8s.io/v1',
|
|
kind: 'ClusterRole',
|
|
metadata: metadata {
|
|
name: metadata.name + '-clusterrole',
|
|
},
|
|
rules: rules,
|
|
},
|
|
|
|
// RoleBinding
|
|
define_role_binding(metadataOrig, roleName, subjects)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'rbac.authorization.k8s.io/v1',
|
|
kind: 'RoleBinding',
|
|
metadata: metadata {
|
|
name: metadata.name + '-rolebinding',
|
|
},
|
|
roleRef: {
|
|
apiGroup: 'rbac.authorization.k8s.io',
|
|
kind: 'Role',
|
|
name: roleName,
|
|
},
|
|
subjects: subjects,
|
|
},
|
|
bind_to_role_sa(role, serviceAccount)::
|
|
$.define_role_binding(role.metadata, role.metadata.name, [{
|
|
kind: 'ServiceAccount',
|
|
name: serviceAccount,
|
|
namespace: role.metadata.namespace,
|
|
}]),
|
|
|
|
// ClusterRoleBinding
|
|
define_cluster_role_binding(metadataOrig, roleName, subjects)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'rbac.authorization.k8s.io/v1',
|
|
kind: 'ClusterRoleBinding',
|
|
metadata: metadata {
|
|
name: metadata.name + '-clusterrolebinding',
|
|
},
|
|
roleRef: {
|
|
apiGroup: 'rbac.authorization.k8s.io',
|
|
kind: 'ClusterRole',
|
|
name: roleName,
|
|
},
|
|
subjects: subjects,
|
|
},
|
|
|
|
// PersistentVolumeClaim
|
|
define_persistent_volume_claim(metadataOrig, storage, access_mode='ReadWriteOnce')::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'v1',
|
|
kind: 'PersistentVolumeClaim',
|
|
metadata: metadata {
|
|
name: metadata.name + '-pvc',
|
|
},
|
|
spec: {
|
|
accessModes: [access_mode],
|
|
resources: {
|
|
requests: {
|
|
storage: storage,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
// ConfigMap
|
|
define_config_map(metadataOrig, data)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'v1',
|
|
kind: 'ConfigMap',
|
|
metadata: metadata {
|
|
name: metadata.name + '-cm',
|
|
},
|
|
data: data,
|
|
},
|
|
|
|
request_cdb_certs_volumes()::
|
|
[
|
|
{
|
|
name: 'client-certs',
|
|
path: '/cockroach-certs',
|
|
emptyDir: true,
|
|
},
|
|
],
|
|
|
|
request_cdb_certs(user)::
|
|
{
|
|
name: 'init-certs',
|
|
image: ociRegistryDocker + '/cockroachdb/cockroach-k8s-request-cert',
|
|
tag: '0.4',
|
|
annotations: {
|
|
'sidecar.istio.io/inject': 'false',
|
|
},
|
|
command: [
|
|
'/bin/ash',
|
|
],
|
|
args: [
|
|
'-ecx',
|
|
'/request-cert -namespace=${POD_NAMESPACE} -certs-dir=/cockroach-certs -type=client -user=' + user + ' -symlink-ca-from=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt && ' +
|
|
'chown -R 1000:1000 /cockroach-certs',
|
|
],
|
|
volumes: $.request_cdb_certs_volumes(),
|
|
env: [{
|
|
name: 'POD_NAMESPACE',
|
|
valueFrom: true,
|
|
field: 'metadata.namespace',
|
|
}],
|
|
},
|
|
|
|
cdb_sa_roles(metadataOrig)::
|
|
local metadata = fix_metadata(metadataOrig);
|
|
{
|
|
apiVersion: 'v1',
|
|
kind: 'List',
|
|
items: [
|
|
$.define_service_account(metadataOrig),
|
|
$.define_role(metadataOrig, [{
|
|
apiGroups: [''],
|
|
resources: ['secrets'],
|
|
verbs: ['create', 'get'],
|
|
}]),
|
|
$.define_role_binding(metadataOrig, metadata.name + '-role', [{
|
|
kind: 'ServiceAccount',
|
|
name: metadata.name + '-serviceaccount',
|
|
namespace: metadata.namespace,
|
|
}]),
|
|
$.define_cluster_role(metadataOrig, [{
|
|
apiGroups: ['certificates.k8s.io'],
|
|
resources: ['certificatesigningrequests'],
|
|
verbs: ['create', 'get', 'watch'],
|
|
}]),
|
|
$.define_cluster_role_binding(metadataOrig, metadata.name + '-clusterrole', [{
|
|
kind: 'ServiceAccount',
|
|
name: metadata.name + '-serviceaccount',
|
|
namespace: metadata.namespace,
|
|
}]),
|
|
],
|
|
},
|
|
|
|
chown_vm(name, path, id, volumes)::
|
|
{
|
|
name: 'chown-vm-' + name,
|
|
image: 'alpine:3.9.3',
|
|
command: [
|
|
'chown',
|
|
'-R',
|
|
'%d:%d' % [id, id],
|
|
path,
|
|
],
|
|
volumes: volumes,
|
|
},
|
|
|
|
istio_labels()::
|
|
{
|
|
'istio-injection': 'enabled',
|
|
},
|
|
|
|
tag(name, extra=false)::
|
|
'%s/%s%s%s%s' % [
|
|
std.strReplace(ociRegistry, 'host.docker.internal.local', 'registry'),
|
|
ociRegistryRepo,
|
|
if ociNoNestedSupport then ':' else '/',
|
|
name,
|
|
if !extra then (if (arch != 'amd64' && !localEnvironment) then '_'+arch else '') else '',
|
|
],
|
|
|
|
tagVersion(name, version)::
|
|
'%s%s%s' % [$.tag(name, true), (if ociNoNestedSupport then '-' else ':'), version],
|
|
|
|
fix_metadata: fix_metadata,
|
|
|
|
prod: prod,
|
|
|
|
dev: dev,
|
|
|
|
version: tag,
|
|
}
|