@@ -22,6 +22,12 @@ type Config struct {
|
|||||||
BackendImage string
|
BackendImage string
|
||||||
LBTelegramImage string
|
LBTelegramImage string
|
||||||
AppDownloadsDir string // OMNEX_APP_DOWNLOADS_DIR : répertoire des .apk téléchargeables (voir internal/downloads)
|
AppDownloadsDir string // OMNEX_APP_DOWNLOADS_DIR : répertoire des .apk téléchargeables (voir internal/downloads)
|
||||||
|
// DockerHubUsername/Password : compte Docker Hub authentifié (droits
|
||||||
|
// lecture seule) injecté dans chaque namespace de démo (voir
|
||||||
|
// deploy/chart-gestion/registry-credentials) pour passer le rate-limit
|
||||||
|
// de pull anonyme (100/6h par IP) à 200/6h. "" = pull anonyme (défaut).
|
||||||
|
DockerHubUsername string
|
||||||
|
DockerHubPassword string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load lit la config. Fail-secure : secret JWT obligatoire ; en prod base + Redis aussi.
|
// Load lit la config. Fail-secure : secret JWT obligatoire ; en prod base + Redis aussi.
|
||||||
@@ -32,21 +38,23 @@ func Load() (Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
Addr: getenv("OMNEX_ADDR", ":8080"),
|
Addr: getenv("OMNEX_ADDR", ":8080"),
|
||||||
JWTSecret: []byte(secret),
|
JWTSecret: []byte(secret),
|
||||||
SessionTTL: 24 * time.Hour,
|
SessionTTL: 24 * time.Hour,
|
||||||
AllowedOrigins: splitCSV(getenv("OMNEX_ALLOWED_ORIGINS", "http://localhost:5173")),
|
AllowedOrigins: splitCSV(getenv("OMNEX_ALLOWED_ORIGINS", "http://localhost:5173")),
|
||||||
Env: getenv("OMNEX_ENV", "dev"),
|
Env: getenv("OMNEX_ENV", "dev"),
|
||||||
DatabaseURL: os.Getenv("OMNEX_DATABASE_URL"),
|
DatabaseURL: os.Getenv("OMNEX_DATABASE_URL"),
|
||||||
RedisURL: os.Getenv("OMNEX_REDIS_URL"),
|
RedisURL: os.Getenv("OMNEX_REDIS_URL"),
|
||||||
DemoDomain: getenv("OMNEX_DEMO_DOMAIN", "demo.omnex.app"),
|
DemoDomain: getenv("OMNEX_DEMO_DOMAIN", "demo.omnex.app"),
|
||||||
DemoHTTPSPort: getenv("OMNEX_DEMO_HTTPS_PORT", "443"),
|
DemoHTTPSPort: getenv("OMNEX_DEMO_HTTPS_PORT", "443"),
|
||||||
RegistrationCode: os.Getenv("OMNEX_REGISTRATION_CODE"),
|
RegistrationCode: os.Getenv("OMNEX_REGISTRATION_CODE"),
|
||||||
Kubeconfig: os.Getenv("KUBECONFIG"),
|
Kubeconfig: os.Getenv("KUBECONFIG"),
|
||||||
FrontendImage: os.Getenv("FRONTEND_IMAGE_APP"),
|
FrontendImage: os.Getenv("FRONTEND_IMAGE_APP"),
|
||||||
BackendImage: os.Getenv("BACKEND_IMAGE_APP"),
|
BackendImage: os.Getenv("BACKEND_IMAGE_APP"),
|
||||||
LBTelegramImage: os.Getenv("LBTELEGRAM_IMAGE_APP"),
|
LBTelegramImage: os.Getenv("LBTELEGRAM_IMAGE_APP"),
|
||||||
AppDownloadsDir: getenv("OMNEX_APP_DOWNLOADS_DIR", "/app-downloads"),
|
AppDownloadsDir: getenv("OMNEX_APP_DOWNLOADS_DIR", "/app-downloads"),
|
||||||
|
DockerHubUsername: os.Getenv("OMNEX_DOCKERHUB_USERNAME"),
|
||||||
|
DockerHubPassword: os.Getenv("OMNEX_DOCKERHUB_PASSWORD"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Env == "prod" {
|
if cfg.Env == "prod" {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"go.yaml.in/yaml/v2"
|
"go.yaml.in/yaml/v2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
k8sCoreV1 "k8s.io/api/core/v1"
|
k8sCoreV1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
@@ -87,8 +88,9 @@ func NewHelmProvisioner(
|
|||||||
// EnsureSharedInfra), jamais par démo — chaque démo installe seulement le
|
// EnsureSharedInfra), jamais par démo — chaque démo installe seulement le
|
||||||
// chart "ingressroute" qui la raccorde à ce Traefik partagé.
|
// chart "ingressroute" qui la raccorde à ce Traefik partagé.
|
||||||
const (
|
const (
|
||||||
sharedTraefikNamespace = "traefik"
|
sharedTraefikNamespace = "traefik"
|
||||||
sharedTraefikRelease = "gestion-traefik"
|
sharedTraefikRelease = "gestion-traefik"
|
||||||
|
sharedRegistryCredentialsRelease = "gestion-registry-credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provision déploie une démo avec Helm :
|
// Provision déploie une démo avec Helm :
|
||||||
@@ -113,6 +115,24 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
|
|||||||
return fmt.Errorf("déploiement network-policy: %w", err)
|
return fmt.Errorf("déploiement network-policy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plafond agrégé de ressources (voir deploy/chart-gestion/resource-quota) :
|
||||||
|
// sans ça, l'autoscaling (HPA) du backend/frontend d'une démo peut monter
|
||||||
|
// en charge sans limite globale et affamer les autres démos du même
|
||||||
|
// cluster — les limites par pod ne bornent que l'individuel, pas l'agrégat.
|
||||||
|
if err := h.installChart(d.Namespace, "resource-quota", nil); err != nil {
|
||||||
|
h.deleteNamespace(d.Namespace)
|
||||||
|
return fmt.Errorf("déploiement resource-quota: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifiants Docker Hub (voir deploy/chart-gestion/registry-credentials) :
|
||||||
|
// AVANT tout chart qui pull une image, sinon les pods restent en
|
||||||
|
// ImagePullBackOff si le rate-limit anonyme est déjà atteint sur ce nœud.
|
||||||
|
// No-op si aucun compte n'est configuré (imagePullSecretsValue == nil).
|
||||||
|
if err := h.installChart(d.Namespace, "registry-credentials", h.buildRegistryCredentialsValues()); err != nil {
|
||||||
|
h.deleteNamespace(d.Namespace)
|
||||||
|
return fmt.Errorf("déploiement registry-credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Mot de passe postgres/redis généré par démo (jamais réutilisé d'une
|
// Mot de passe postgres/redis généré par démo (jamais réutilisé d'une
|
||||||
// démo à l'autre) : avec des identifiants partagés, un gap réseau
|
// démo à l'autre) : avec des identifiants partagés, un gap réseau
|
||||||
// (CNI, règle manquante) donnerait un accès direct aux données de
|
// (CNI, règle manquante) donnerait un accès direct aux données de
|
||||||
@@ -207,11 +227,29 @@ func (h *HelmProvisioner) EnsureSharedInfra() error {
|
|||||||
return fmt.Errorf("création namespace %s: %w", sharedTraefikNamespace, err)
|
return fmt.Errorf("création namespace %s: %w", sharedTraefikNamespace, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identifiants Docker Hub (voir deploy/chart-gestion/registry-credentials) :
|
||||||
|
// AVANT traefik, qui pull son image + le plugin WASM Coraza. No-op si
|
||||||
|
// aucun compte n'est configuré.
|
||||||
|
if err := h.upgradeInstallChart(sharedTraefikNamespace, sharedRegistryCredentialsRelease, "registry-credentials", h.buildRegistryCredentialsValues()); err != nil {
|
||||||
|
return fmt.Errorf("déploiement registry-credentials partagé: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Le subchart officiel traefik/traefik est vendorisé dans le repo
|
// Le subchart officiel traefik/traefik est vendorisé dans le repo
|
||||||
// (deploy/chart-gestion/traefik/charts/traefik-*.tgz + Chart.lock) :
|
// (deploy/chart-gestion/traefik/charts/traefik-*.tgz + Chart.lock) :
|
||||||
// pas de "helm dependency update" au runtime, qui échouerait de toute
|
// pas de "helm dependency update" au runtime, qui échouerait de toute
|
||||||
// façon puisque /charts est monté en lecture seule.
|
// façon puisque /charts est monté en lecture seule.
|
||||||
if err := h.upgradeInstallChart(sharedTraefikNamespace, sharedTraefikRelease, "traefik", nil); err != nil {
|
// "deployment.imagePullSecrets", pas "imagePullSecrets" à la racine :
|
||||||
|
// emplacement imposé par values.schema.json du chart officiel
|
||||||
|
// traefik/traefik (voir le commentaire équivalent dans
|
||||||
|
// deploy/chart-gestion/traefik/values.yaml).
|
||||||
|
traefikValues := map[string]interface{}{
|
||||||
|
"traefik": map[string]interface{}{
|
||||||
|
"deployment": map[string]interface{}{
|
||||||
|
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := h.upgradeInstallChart(sharedTraefikNamespace, sharedTraefikRelease, "traefik", traefikValues); err != nil {
|
||||||
return fmt.Errorf("déploiement traefik partagé: %w", err)
|
return fmt.Errorf("déploiement traefik partagé: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +316,14 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
|
|||||||
h.deleteNamespace(newNamespace)
|
h.deleteNamespace(newNamespace)
|
||||||
return fmt.Errorf("déploiement network-policy: %w", err)
|
return fmt.Errorf("déploiement network-policy: %w", err)
|
||||||
}
|
}
|
||||||
|
if err := h.installChart(newNamespace, "resource-quota", nil); err != nil {
|
||||||
|
h.deleteNamespace(newNamespace)
|
||||||
|
return fmt.Errorf("déploiement resource-quota: %w", err)
|
||||||
|
}
|
||||||
|
if err := h.installChart(newNamespace, "registry-credentials", h.buildRegistryCredentialsValues()); err != nil {
|
||||||
|
h.deleteNamespace(newNamespace)
|
||||||
|
return fmt.Errorf("déploiement registry-credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
newDemo := d
|
newDemo := d
|
||||||
newDemo.Namespace = newNamespace
|
newDemo.Namespace = newNamespace
|
||||||
@@ -648,7 +694,23 @@ func (h *HelmProvisioner) execInPod(ctx context.Context, namespace, pod, contain
|
|||||||
return stdout.String(), stderrBuf.String(), nil
|
return stdout.String(), stderrBuf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createNamespace crée un namespace Kubernetes.
|
// omnexServiceAccountNamespace / omnexServiceAccountName / omnexWorkloadClusterRole :
|
||||||
|
// identité du ServiceAccount du control-plane (voir deploy/rbac/control-plane.yml).
|
||||||
|
// omnexWorkloadClusterRole n'est JAMAIS liée cluster-wide — seulement via un
|
||||||
|
// RoleBinding par namespace (voir ensureWorkloadRoleBinding), pour que le
|
||||||
|
// ServiceAccount n'ait aucun accès aux namespaces qu'il ne gère pas
|
||||||
|
// (cert-manager, longhorn-system, kube-system...). Le ServiceAccount ne
|
||||||
|
// possède que le verbe "bind" sur cette ClusterRole précise : il ne peut pas
|
||||||
|
// s'octroyer davantage (mécanisme RBAC anti-escalade de Kubernetes).
|
||||||
|
const (
|
||||||
|
omnexServiceAccountNamespace = "omnex-system"
|
||||||
|
omnexServiceAccountName = "omnex-control-plane"
|
||||||
|
omnexWorkloadClusterRole = "omnex-demo-workload-manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createNamespace crée un namespace Kubernetes et y lie la ClusterRole
|
||||||
|
// métier du control-plane (moindre privilège — voir la doc des constantes
|
||||||
|
// ci-dessus).
|
||||||
func (h *HelmProvisioner) createNamespace(name string) error {
|
func (h *HelmProvisioner) createNamespace(name string) error {
|
||||||
ns := &k8sCoreV1.Namespace{
|
ns := &k8sCoreV1.Namespace{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@@ -662,6 +724,34 @@ func (h *HelmProvisioner) createNamespace(name string) error {
|
|||||||
if err != nil && !k8sErrors.IsAlreadyExists(err) {
|
if err != nil && !k8sErrors.IsAlreadyExists(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return h.ensureWorkloadRoleBinding(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureWorkloadRoleBinding lie omnexWorkloadClusterRole au ServiceAccount du
|
||||||
|
// control-plane, dans "namespace" uniquement — c'est ce RoleBinding (pas une
|
||||||
|
// ClusterRoleBinding) qui borne les droits du control-plane à ce seul
|
||||||
|
// namespace pour les Deployments/Secrets/ConfigMaps/etc.
|
||||||
|
func (h *HelmProvisioner) ensureWorkloadRoleBinding(namespace string) error {
|
||||||
|
rb := &rbacv1.RoleBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: omnexServiceAccountName,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
RoleRef: rbacv1.RoleRef{
|
||||||
|
APIGroup: "rbac.authorization.k8s.io",
|
||||||
|
Kind: "ClusterRole",
|
||||||
|
Name: omnexWorkloadClusterRole,
|
||||||
|
},
|
||||||
|
Subjects: []rbacv1.Subject{{
|
||||||
|
Kind: "ServiceAccount",
|
||||||
|
Name: omnexServiceAccountName,
|
||||||
|
Namespace: omnexServiceAccountNamespace,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
_, err := h.k8sClient.RbacV1().RoleBindings(namespace).Create(context.Background(), rb, metav1.CreateOptions{})
|
||||||
|
if err != nil && !k8sErrors.IsAlreadyExists(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,6 +783,13 @@ func (h *HelmProvisioner) installChart(namespace, chartName string, values map[s
|
|||||||
"--wait",
|
"--wait",
|
||||||
"--atomic",
|
"--atomic",
|
||||||
"--timeout", "5m",
|
"--timeout", "5m",
|
||||||
|
// Le ServiceAccount du control-plane n'a volontairement aucun droit
|
||||||
|
// cluster-scope sur les CustomResourceDefinition (voir
|
||||||
|
// deploy/rbac/control-plane.yml) : les CRD sont appliquées une fois,
|
||||||
|
// à part, par un opérateur humain avec un accès admin — jamais par
|
||||||
|
// ce chemin. Sans ce flag, "helm install" tente quand même de les
|
||||||
|
// créer (même si elles existent déjà) et échoue en Forbidden.
|
||||||
|
"--skip-crds",
|
||||||
}
|
}
|
||||||
if err := h.runHelm(args...); err != nil {
|
if err := h.runHelm(args...); err != nil {
|
||||||
return fmt.Errorf("helm install %s: %w", chartName, err)
|
return fmt.Errorf("helm install %s: %w", chartName, err)
|
||||||
@@ -714,6 +811,12 @@ func (h *HelmProvisioner) upgradeInstallChart(namespace, release, chartName stri
|
|||||||
"--wait",
|
"--wait",
|
||||||
"--atomic",
|
"--atomic",
|
||||||
"--timeout", "5m",
|
"--timeout", "5m",
|
||||||
|
// Voir le commentaire équivalent dans installChart : le
|
||||||
|
// ServiceAccount du control-plane n'a pas le droit de créer des CRD
|
||||||
|
// (deploy/rbac/control-plane.yml) — utilisé ici pour le chart
|
||||||
|
// "traefik", dont le subchart vendorisé embarque des CRD déjà
|
||||||
|
// appliquées une fois à part par un opérateur admin.
|
||||||
|
"--skip-crds",
|
||||||
}
|
}
|
||||||
if values != nil {
|
if values != nil {
|
||||||
valuesFile, cleanup, err := h.writeValuesFile(namespace, chartName, values)
|
valuesFile, cleanup, err := h.writeValuesFile(namespace, chartName, values)
|
||||||
@@ -765,6 +868,38 @@ func (h *HelmProvisioner) runHelmOutput(args ...string) ([]byte, error) {
|
|||||||
return stdout.Bytes(), nil
|
return stdout.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dockerHubSecretName : nom fixe du Secret créé par le chart
|
||||||
|
// registry-credentials (voir buildRegistryCredentialsValues) — référencé
|
||||||
|
// tel quel par imagePullSecretsValue, toujours dans le même namespace.
|
||||||
|
const dockerHubSecretName = "dockerhub-pull-secret"
|
||||||
|
|
||||||
|
// imagePullSecretsValue : nil si aucun compte Docker Hub n'est configuré
|
||||||
|
// (OMNEX_DOCKERHUB_USERNAME/PASSWORD vides — pull anonyme, comportement par
|
||||||
|
// défaut inchangé). Un map Helm avec une valeur nil pour une clé existante
|
||||||
|
// du chart fait retomber sur le défaut du chart ("[]"), donc sans danger à
|
||||||
|
// assigner inconditionnellement.
|
||||||
|
func (h *HelmProvisioner) imagePullSecretsValue() []map[string]interface{} {
|
||||||
|
if h.cfg.DockerHubUsername == "" || h.cfg.DockerHubPassword == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []map[string]interface{}{{"name": dockerHubSecretName}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRegistryCredentialsValues construit les valeurs pour le chart
|
||||||
|
// registry-credentials (voir Provision/EnsureSharedInfra/
|
||||||
|
// MigrateToPremiumNamespace) — nil si pas de compte configuré, auquel cas
|
||||||
|
// installChart écrit un fichier de valeurs "null" (no-op, voir writeValuesFile).
|
||||||
|
func (h *HelmProvisioner) buildRegistryCredentialsValues() map[string]interface{} {
|
||||||
|
if h.cfg.DockerHubUsername == "" || h.cfg.DockerHubPassword == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"secretName": dockerHubSecretName,
|
||||||
|
"username": h.cfg.DockerHubUsername,
|
||||||
|
"password": h.cfg.DockerHubPassword,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseImage(image string) (repo string, tag string) {
|
func parseImage(image string) (repo string, tag string) {
|
||||||
if image == "" {
|
if image == "" {
|
||||||
return "", "helm"
|
return "", "helm"
|
||||||
@@ -860,7 +995,8 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
|
|||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]interface{}{
|
values := map[string]interface{}{
|
||||||
"replicaCount": 1,
|
"replicaCount": 1,
|
||||||
|
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||||
"image": map[string]interface{}{
|
"image": map[string]interface{}{
|
||||||
"repository": backendRepo,
|
"repository": backendRepo,
|
||||||
"tag": backendTag,
|
"tag": backendTag,
|
||||||
@@ -894,7 +1030,8 @@ func (h *HelmProvisioner) buildFrontendValues(d Demo) map[string]interface{} {
|
|||||||
frontendRepo, frontendTag := parseImage(h.frontendImage)
|
frontendRepo, frontendTag := parseImage(h.frontendImage)
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"replicaCount": 1,
|
"replicaCount": 1,
|
||||||
|
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||||
"image": map[string]interface{}{
|
"image": map[string]interface{}{
|
||||||
"repository": frontendRepo,
|
"repository": frontendRepo,
|
||||||
"tag": frontendTag,
|
"tag": frontendTag,
|
||||||
@@ -977,6 +1114,7 @@ func (h *HelmProvisioner) buildLBTelegramValues(d Demo, cfg ProvisionConfig, bac
|
|||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
|
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||||
"image": map[string]interface{}{
|
"image": map[string]interface{}{
|
||||||
"repository": lbRepo,
|
"repository": lbRepo,
|
||||||
"tag": lbTag,
|
"tag": lbTag,
|
||||||
@@ -1028,6 +1166,7 @@ func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]
|
|||||||
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
|
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
|
||||||
func (h *HelmProvisioner) buildPostgresValues(d Demo, password string) map[string]interface{} {
|
func (h *HelmProvisioner) buildPostgresValues(d Demo, password string) map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
|
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||||
"auth": map[string]interface{}{
|
"auth": map[string]interface{}{
|
||||||
"password": password,
|
"password": password,
|
||||||
"username": "postgres",
|
"username": "postgres",
|
||||||
@@ -1048,6 +1187,7 @@ func (h *HelmProvisioner) buildPostgresValues(d Demo, password string) map[strin
|
|||||||
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
|
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
|
||||||
func (h *HelmProvisioner) buildRedisValues(d Demo, password string) map[string]interface{} {
|
func (h *HelmProvisioner) buildRedisValues(d Demo, password string) map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
|
"imagePullSecrets": h.imagePullSecretsValue(),
|
||||||
"service": map[string]interface{}{
|
"service": map[string]interface{}{
|
||||||
"type": "ClusterIP",
|
"type": "ClusterIP",
|
||||||
"port": 6379,
|
"port": 6379,
|
||||||
|
|||||||
Reference in New Issue
Block a user