feat: harbor integration — EnsureProject + per-namespace image path

- internal/harbor/client.go: новый пакет, EnsureProject (GET+POST idempotent)
- config.go: добавлены HarborUser/HarborPass (из HARBOR_USER/HARBOR_PASS env)
- builder.go: Projecter интерфейс, harborClient поле, ImageRef: {host}/{ns}/{func}:{tag}
  EnsureProject вызывается перед каждым Build()
- function_controller.go: HarborClient поле, EnsureProject при создании k8s NS
- main.go: создание harbor.Client если HARBOR_USER+HARBOR_PASS заданы
- operator.yaml: REGISTRY_HOST=pearlharbor..., HARBOR_USER=admin, v0.1.24
- hack/create-registry-secret.sh: переписан для Harbor (HARBOR_USER/HARBOR_PASS)

Смена registry — только через REGISTRY_HOST в ConfigMap, больше нигде.
This commit is contained in:
“Naeel”
2026-03-11 14:58:04 +04:00
parent 6443f21ddc
commit babd8e6109
7 changed files with 185 additions and 34 deletions
+12 -3
View File
@@ -1,4 +1,4 @@
// Изменено: 2026-03-08 // Изменено: 2026-03-11
// FunctionReconciler — основной контроллер оператора. // FunctionReconciler — основной контроллер оператора.
// Следит за CRD Function и управляет lifecycle функции: // Следит за CRD Function и управляет lifecycle функции:
// Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed // Pending → Building (запуск kaniko Job) → Ready (образ собран, Deployment создан) / Failed
@@ -25,6 +25,7 @@ import (
slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1" slessv1alpha1 "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/api/v1alpha1"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/harbor"
) )
// FunctionReconciler reconciles a Function object // FunctionReconciler reconciles a Function object
@@ -32,8 +33,9 @@ type FunctionReconciler struct {
client.Client client.Client
Scheme *runtime.Scheme Scheme *runtime.Scheme
Builder *builder.Builder Builder *builder.Builder
RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций) RegistrySecret string // имя Secret с docker credentials (для imagePullSecrets в подах функций)
OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-* OperatorNamespace string // namespace оператора — откуда копируем RegistrySecret в sless-fn-*
HarborClient *harbor.Client // nil — Harbor не используется, EnsureProject пропускается
} }
//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=sless.kube5s.ru,resources=functions,verbs=get;list;watch;create;update;patch;delete
@@ -183,6 +185,13 @@ func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1al
if err := r.Create(ctx, ns); err != nil { if err := r.Create(ctx, ns); err != nil {
return ctrl.Result{}, fmt.Errorf("create function namespace: %w", err) return ctrl.Result{}, fmt.Errorf("create function namespace: %w", err)
} }
// Создаём Harbor-проект для namespace сразу при создании k8s NS (best-effort).
// Если не удалось — EnsureProject повторит вызов внутри Build().
if r.HarborClient != nil {
if err := r.HarborClient.EnsureProject(ctx, fn.Namespace); err != nil {
log.FromContext(ctx).Error(err, "harbor ensure project on ns create", "project", fn.Namespace)
}
}
} else { } else {
return ctrl.Result{}, fmt.Errorf("get function namespace: %w", err) return ctrl.Result{}, fmt.Errorf("get function namespace: %w", err)
} }
+15 -13
View File
@@ -1,20 +1,14 @@
# Изменено: 2026-03-07 # Изменено: 2026-03-11
# Деплой sless оператора в кластер. # Деплой sless оператора в кластер.
# Состав: # Состав:
# - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.) # - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.)
# - Secret: секретные данные (S3 keys, postgres DSN, API token, docker auth) # - Secret: секретные данные (S3 keys, postgres DSN, API token, Harbor pass)
# - Deployment: оператор naeel/sless-operator:v0.1.23 в namespace sless # - Deployment: оператор naeel/sless-operator:v0.1.23 в namespace sless
# - Service: ClusterIP :9090 (REST API) # - Service: ClusterIP :9090 (REST API)
# - Ingress: sless-api.kube5s.ru → :9090 (внешний доступ с TLS) # - Ingress: sless-api.kube5s.ru → :9090 (внешний доступ с TLS)
# #
# Перед применением: # Чтобы сменить registry — менять только REGISTRY_HOST в ConfigMap.
# kubectl create secret generic sless-operator-secret \ # Чтобы сменить Harbor-аккаунт — менять HARBOR_USER в ConfigMap + HARBOR_PASS в Secret.
# --namespace=sless \
# --from-literal=POSTGRES_DSN="..." \
# --from-literal=S3_ACCESS_KEY="..." \
# --from-literal=S3_SECRET_KEY="..." \
# --from-literal=SLESS_API_TOKEN="change-me" \
# --dry-run=client -o yaml | kubectl apply -f -
# #
# Применение: kubectl apply -f deployments/k8s/operator.yaml # Применение: kubectl apply -f deployments/k8s/operator.yaml
--- ---
@@ -27,8 +21,13 @@ data:
S3_ENDPOINT: "s3.msk-1.ngcloud.ru" S3_ENDPOINT: "s3.msk-1.ngcloud.ru"
S3_BUCKET: "sless-functions" S3_BUCKET: "sless-functions"
S3_USE_SSL: "true" S3_USE_SSL: "true"
REGISTRY_HOST: "naeel" # REGISTRY_HOST — единственное место, где прописан адрес registry.
# Чтобы сменить реестр — менять только здесь.
# Harbor: pearlharbor.registryk8s.services.ngcloud.ru
# DockerHub (legacy): naeel
REGISTRY_HOST: "pearlharbor.registryk8s.services.ngcloud.ru"
REGISTRY_SECRET: "sless-registry-auth" REGISTRY_SECRET: "sless-registry-auth"
HARBOR_USER: "admin"
API_PORT: "9090" API_PORT: "9090"
INGRESS_HOST: "fn.kube5s.ru" INGRESS_HOST: "fn.kube5s.ru"
# EXTERNAL_URL — если задан, URL функции = EXTERNAL_URL/fn/{namespace}/{name} # EXTERNAL_URL — если задан, URL функции = EXTERNAL_URL/fn/{namespace}/{name}
@@ -41,13 +40,16 @@ data:
# S3_ACCESS_KEY — ключ доступа к S3/Ceph # S3_ACCESS_KEY — ключ доступа к S3/Ceph
# S3_SECRET_KEY — секретный ключ S3/Ceph # S3_SECRET_KEY — секретный ключ S3/Ceph
# SLESS_API_TOKEN — токен аутентификации API # SLESS_API_TOKEN — токен аутентификации API
# HARBOR_PASS — пароль Harbor API (для EnsureProject, не для kaniko push)
# #
# Пример создания: # Пример создания:
# kubectl create secret generic sless-operator-secret -n sless \ # kubectl create secret generic sless-operator-secret -n sless \
# --from-literal=POSTGRES_DSN="postgres://sless:PASSWORD@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" \ # --from-literal=POSTGRES_DSN="postgres://sless:PASSWORD@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" \
# --from-literal=S3_ACCESS_KEY="ACCESS_KEY" \ # --from-literal=S3_ACCESS_KEY="ACCESS_KEY" \
# --from-literal=S3_SECRET_KEY="SECRET_KEY" \ # --from-literal=S3_SECRET_KEY="SECRET_KEY" \
# --from-literal=SLESS_API_TOKEN="your-token-here" # --from-literal=SLESS_API_TOKEN="your-token-here" \
# --from-literal=HARBOR_PASS="harbor-admin-password"
# kubectl apply -f hack/create-registry-secret.sh # docker-креды для kaniko
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
@@ -70,7 +72,7 @@ spec:
containers: containers:
- name: operator - name: operator
# При обновлении версии оператора — менять тег здесь (не latest!) # При обновлении версии оператора — менять тег здесь (не latest!)
image: naeel/sless-operator:v0.1.23 image: naeel/sless-operator:v0.1.24
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый) # Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
+15 -11
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Изменено: 2026-03-07 # Изменено: 2026-03-11
# Создаёт k8s Secret sless-registry-auth с DockerHub кредами для kaniko. # Создаёт k8s Secret sless-registry-auth с кредами Harbor для kaniko.
# Kaniko монтирует этот secret как /kaniko/.docker/config.json для push образов. # Kaniko монтирует этот secret как /kaniko/.docker/config.json для push образов.
# Отдельно от HARBOR_PASS в operator-secret: этот secret — для kaniko (push),
# HARBOR_PASS — для Harbor API (создание проектов через EnsureProject).
# #
# Использование: # Использование:
# DOCKER_HUB_USER=naeel DOCKER_HUB_TOKEN=<token> ./hack/create-registry-secret.sh # HARBOR_USER=admin HARBOR_PASS=<password> REGISTRY=pearlharbor.registryk8s.services.ngcloud.ru \
# ./hack/create-registry-secret.sh
# #
# Требуется: kubectl, авторизованный context с доступом к namespace sless. # Требуется: kubectl, авторизованный context с доступом к namespace sless.
@@ -12,21 +15,22 @@ set -euo pipefail
NAMESPACE="sless" NAMESPACE="sless"
SECRET_NAME="sless-registry-auth" SECRET_NAME="sless-registry-auth"
REGISTRY="${REGISTRY:-pearlharbor.registryk8s.services.ngcloud.ru}"
if [[ -z "${DOCKER_HUB_USER:-}" ]]; then if [[ -z "${HARBOR_USER:-}" ]]; then
echo "ERROR: DOCKER_HUB_USER env var is required" echo "ERROR: HARBOR_USER env var is required"
exit 1 exit 1
fi fi
if [[ -z "${DOCKER_HUB_TOKEN:-}" ]]; then if [[ -z "${HARBOR_PASS:-}" ]]; then
echo "ERROR: DOCKER_HUB_TOKEN env var is required (DockerHub access token, not password)" echo "ERROR: HARBOR_PASS env var is required"
exit 1 exit 1
fi fi
kubectl create secret docker-registry "${SECRET_NAME}" \ kubectl create secret docker-registry "${SECRET_NAME}" \
--docker-username="${DOCKER_HUB_USER}" \ --docker-username="${HARBOR_USER}" \
--docker-password="${DOCKER_HUB_TOKEN}" \ --docker-password="${HARBOR_PASS}" \
--docker-server="https://index.docker.io/v1/" \ --docker-server="${REGISTRY}" \
--namespace="${NAMESPACE}" \ --namespace="${NAMESPACE}" \
--dry-run=client -o yaml | kubectl apply -f - --dry-run=client -o yaml | kubectl apply -f -
echo "OK: secret '${SECRET_NAME}' applied in namespace '${NAMESPACE}'" echo "OK: secret '${SECRET_NAME}' applied in namespace '${NAMESPACE}' (server: ${REGISTRY})"
+26 -5
View File
@@ -1,9 +1,9 @@
// Изменено: 2026-03-07 // Изменено: 2026-03-11
// Builder — запускает kaniko Job в k8s для сборки Docker образа из кода функции. // Builder — запускает kaniko Job в k8s для сборки Docker образа из кода функции.
// Почему kaniko, а не docker-in-docker (DinD): // Почему kaniko, а не docker-in-docker (DinD):
// kaniko не требует privileged контейнер, что безопаснее в managed кластере. // kaniko не требует privileged контейнер, что безопаснее в managed кластере.
// kaniko читает контекст сборки из S3 напрямую. // kaniko читает контекст сборки из S3 напрямую.
// Workflow: FunctionController вызывает Build → Job запускается → образ пушится в registry. // Workflow: FunctionController вызывает Build → EnsureProject (Harbor) → Job запускается → образ пушится в registry.
package builder package builder
@@ -20,17 +20,24 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
) )
// Projecter — опциональный интерфейс для реестров с поддержкой проектов (Harbor).
// Если nil — вызов EnsureProject пропускается (Docker Hub, другие реестры без проектов).
type Projecter interface {
EnsureProject(ctx context.Context, name string) error
}
// Builder — управляет сборкой Docker образов через kaniko Jobs в k8s. // Builder — управляет сборкой Docker образов через kaniko Jobs в k8s.
type Builder struct { type Builder struct {
client client.Client client client.Client
builderImage string // образ kaniko builderImage string // образ kaniko
registryHost string // куда пушим образ registryHost string // куда пушим образ (Harbor: host; DockerHub: user/org)
registrySecret string // имя k8s Secret с docker-кредами для kaniko registrySecret string // имя k8s Secret с docker-кредами для kaniko
s3Endpoint string // откуда kaniko берёт код s3Endpoint string // откуда kaniko берёт код
s3AccessKey string s3AccessKey string
s3SecretKey string s3SecretKey string
s3Bucket string s3Bucket string
namespace string // namespace где запускаем build Job'ы namespace string // namespace где запускаем build Job'ы
harborClient Projecter // nil если Harbor не используется
} }
// Config — параметры для создания Builder'а. // Config — параметры для создания Builder'а.
@@ -43,6 +50,7 @@ type Config struct {
S3SecretKey string S3SecretKey string
S3Bucket string S3Bucket string
Namespace string Namespace string
HarborClient Projecter // nil — Harbor не используется, EnsureProject пропускается
} }
// New создаёт новый Builder. // New создаёт новый Builder.
@@ -57,21 +65,34 @@ func New(c client.Client, cfg Config) *Builder {
s3SecretKey: cfg.S3SecretKey, s3SecretKey: cfg.S3SecretKey,
s3Bucket: cfg.S3Bucket, s3Bucket: cfg.S3Bucket,
namespace: cfg.Namespace, namespace: cfg.Namespace,
harborClient: cfg.HarborClient,
} }
} }
// ImageRef возвращает полный путь к образу в registry для данной функции и версии. // ImageRef возвращает полный путь к образу в registry для данной функции и версии.
// Тег = первые 12 символов sha256(s3Key): уникален per build, не меняется при // Тег = первые 12 символов sha256(s3Key): уникален per build, не меняется при
// повторном reconcile с тем же s3Key, не требует хаков с :latest. // повторном reconcile с тем же s3Key, не требует хаков с :latest.
// Формат: {registryHost}/{namespace}/{funcName}:{tag}
//
// Harbor: pearlharbor.registryk8s.services.ngcloud.ru/{ns}/{func}:{tag}
//
// Чтобы сменить реестр — достаточно изменить REGISTRY_HOST в одном месте (ConfigMap).
func (b *Builder) ImageRef(namespace, funcName, s3Key string) string { func (b *Builder) ImageRef(namespace, funcName, s3Key string) string {
h := sha256.Sum256([]byte(s3Key)) h := sha256.Sum256([]byte(s3Key))
tag := fmt.Sprintf("%x", h[:6]) // 12 hex-символов tag := fmt.Sprintf("%x", h[:6]) // 12 hex-символов
return fmt.Sprintf("%s/sless-%s-%s:%s", b.registryHost, namespace, funcName, tag) return fmt.Sprintf("%s/%s/%s:%s", b.registryHost, namespace, funcName, tag)
} }
// Build запускает kaniko Job для сборки образа функции. // Build запускает kaniko Job для сборки образа функции.
// Возвращает имя Job'а чтобы контроллер мог следить за его статусом. // Возвращает имя Job'а чтобы контроллер мог следить за его статусом.
func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string) (string, error) { func (b *Builder) Build(ctx context.Context, namespace, funcName, s3Key string) (string, error) {
// Идемпотентно создаём Harbor-проект перед push — kaniko сам его не создаёт.
// Пропускаем если HarborClient не задан (Docker Hub и другие реестры без проектов).
if b.harborClient != nil {
if err := b.harborClient.EnsureProject(ctx, namespace); err != nil {
return "", fmt.Errorf("harbor ensure project %q: %w", namespace, err)
}
}
imageRef := b.ImageRef(namespace, funcName, s3Key) imageRef := b.ImageRef(namespace, funcName, s3Key)
jobName := fmt.Sprintf("build-%s-%s", funcName, time.Now().Format("20060102150405")) jobName := fmt.Sprintf("build-%s-%s", funcName, time.Now().Format("20060102150405"))
+11 -2
View File
@@ -50,10 +50,14 @@ type Config struct {
// Поле сохранено для совместимости конфигурации. // Поле сохранено для совместимости конфигурации.
// Аутентификация происходит в middleware/auth.go через validateJWT(). // Аутентификация происходит в middleware/auth.go через validateJWT().
APIToken string APIToken string
// Harbor — креды для Harbor API (создание проектов до push образов).
// REGISTRY_HOST уже содержит адрес реестра — дублировать не нужно.
// Если не заданы, EnsureProject пропускается (только DockerHub/без проектов).
HarborUser string
HarborPass string
} }
// Load читает конфиг из env переменных.
// Возвращает ошибку если обязательные переменные не заданы.
func Load() (*Config, error) { func Load() (*Config, error) {
cfg := &Config{ cfg := &Config{
APIPort: 8080, APIPort: 8080,
@@ -128,5 +132,10 @@ func Load() (*Config, error) {
// Не required: auth middleware использует validateJWT, а не статический токен. // Не required: auth middleware использует validateJWT, а не статический токен.
cfg.APIToken = os.Getenv("SLESS_API_TOKEN") cfg.APIToken = os.Getenv("SLESS_API_TOKEN")
// Harbor API креды — опциональны.
// Если не заданы, EnsureProject пропускается (push работает через docker-кред в kaniko).
cfg.HarborUser = os.Getenv("HARBOR_USER")
cfg.HarborPass = os.Getenv("HARBOR_PASS")
return cfg, nil return cfg, nil
} }
+94
View File
@@ -0,0 +1,94 @@
// Изменено: 2026-03-11
// Harbor API клиент — управление проектами (create/ensure) перед push образов.
// Почему здесь: kaniko не создаёт Harbor-проект сам, нужен явный вызов API.
// Все операции идемпотентны - повторный вызов EnsureProject безопасен.
package harbor
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// Client — минимальный HTTP-клиент для Harbor API v2.
// Используется только для создания проектов; пуш образов делает kaniko напрямую.
type Client struct {
baseURL string // https://{registryHost}
user string
password string
http *http.Client
}
// New создаёт Harbor клиент.
// registryHost — хост реестра без схемы (e.g. "pearlharbor.registryk8s.services.ngcloud.ru").
func New(registryHost, user, password string) *Client {
return &Client{
baseURL: "https://" + registryHost,
user: user,
password: password,
http: &http.Client{Timeout: 15 * time.Second},
}
}
// EnsureProject идемпотентно создаёт Harbor проект с именем name (private).
// GET /api/v2.0/projects?name={name} → если не найден → POST /api/v2.0/projects.
// Возвращает nil если проект уже существовал или был успешно создан.
func (c *Client) EnsureProject(ctx context.Context, name string) error {
// Harbor GET projects возвращает список проектов по фильтру name=
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
fmt.Sprintf("%s/api/v2.0/projects?name=%s", c.baseURL, name), nil)
if err != nil {
return fmt.Errorf("harbor: build GET request: %w", err)
}
req.SetBasicAuth(c.user, c.password)
resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("harbor: GET project %q: %w", name, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("harbor: unauthorized — проверьте HARBOR_USER и HARBOR_PASS")
}
var projects []struct {
Name string `json:"name"`
}
if err := json.NewDecoder(resp.Body).Decode(&projects); err != nil {
return fmt.Errorf("harbor: decode GET response: %w", err)
}
for _, p := range projects {
if p.Name == name {
return nil // проект уже существует
}
}
// Проект не найден — создаём
body, _ := json.Marshal(map[string]interface{}{
"project_name": name,
"public": false,
})
req, err = http.NewRequestWithContext(ctx, http.MethodPost,
c.baseURL+"/api/v2.0/projects", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("harbor: build POST request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(c.user, c.password)
resp2, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("harbor: POST project %q: %w", name, err)
}
defer resp2.Body.Close()
// 201 Created — успешно; 409 Conflict — гонка (кто-то создал параллельно), оба ок
if resp2.StatusCode != http.StatusCreated && resp2.StatusCode != http.StatusConflict {
return fmt.Errorf("harbor: POST project %q: unexpected status %d", name, resp2.StatusCode)
}
return nil
}
+12
View File
@@ -31,6 +31,7 @@ import (
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/api/handler" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/api/handler"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/builder"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/config" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/config"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/harbor"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/postgres" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/postgres"
"gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/s3" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/s3"
//+kubebuilder:scaffold:imports //+kubebuilder:scaffold:imports
@@ -118,6 +119,15 @@ func main() {
} }
// Builder использует k8s client из manager'а // Builder использует k8s client из manager'а
// harborClient — nil если HARBOR_USER/HARBOR_PASS не заданы (EnsureProject пропускается).
var harborClient *harbor.Client
if cfg.HarborUser != "" && cfg.HarborPass != "" {
harborClient = harbor.New(cfg.RegistryHost, cfg.HarborUser, cfg.HarborPass)
log.Info("harbor client configured", "host", cfg.RegistryHost)
} else {
log.Info("harbor client disabled (HARBOR_USER/HARBOR_PASS not set)")
}
bldr := builder.New(mgr.GetClient(), builder.Config{ bldr := builder.New(mgr.GetClient(), builder.Config{
BuilderImage: cfg.BuilderImage, BuilderImage: cfg.BuilderImage,
RegistryHost: cfg.RegistryHost, RegistryHost: cfg.RegistryHost,
@@ -127,6 +137,7 @@ func main() {
S3SecretKey: cfg.S3SecretKey, S3SecretKey: cfg.S3SecretKey,
S3Bucket: cfg.S3Bucket, S3Bucket: cfg.S3Bucket,
Namespace: "sless", Namespace: "sless",
HarborClient: harborClient,
}) })
if err = (&controllers.FunctionReconciler{ if err = (&controllers.FunctionReconciler{
@@ -135,6 +146,7 @@ func main() {
Builder: bldr, Builder: bldr,
RegistrySecret: cfg.RegistrySecret, RegistrySecret: cfg.RegistrySecret,
OperatorNamespace: "sless", OperatorNamespace: "sless",
HarborClient: harborClient,
}).SetupWithManager(mgr); err != nil { }).SetupWithManager(mgr); err != nil {
log.Error("unable to create controller", "controller", "Function", "err", err) log.Error("unable to create controller", "controller", "Function", "err", err)
os.Exit(1) os.Exit(1)