feat(config): initialize namespaces from configuration

Replace the hard-coded library namespace seed with an idempotent bootstrap
task driven by a new namespace.initialize config list, so users can declare
any number of namespaces and set the visibility of each one (public or
private). The task only creates namespaces that do not exist yet.

Remove the seed INSERT from the four 0001 migrations and wire the new config
into the example configs, the docs and the Helm chart (values + configmap).
This commit is contained in:
Tosone committed 2026-09-27 11:11:04 +08:00
1 parent 93cf7c1946
commit c70127b0d1
16 files changed
+248 -70

No files matched your search

+5
View File
@@ -65,6 +65,11 @@ namespace:
autoCreate: false
# the automatic created namespace visibility, available: public, private
visibility: public
# namespaces created at startup; each entry is created when missing,
# and each entry may set its own visibility (public or private)
initialize:
- name: library
visibility: public
http:
# endpoint can be a domain or domain with port, eg: http://sigma.test.io, https://sigma.test.io:30080, http://127.0.0.1:3000
+5
View File
@@ -67,6 +67,11 @@ namespace:
autoCreate: true
# the automatic created namespace visibility, available: public, private
visibility: public
# namespaces created at startup; each entry is created when missing,
# and each entry may set its own visibility (public or private)
initialize:
- name: library
visibility: public
http:
# endpoint can be a domain or domain with port, eg: http://sigma.test.io, https://sigma.test.io:30080, http://127.0.0.1:3000
+5
View File
@@ -71,6 +71,11 @@ namespace:
autoCreate: false
# the automatic created namespace visibility, available: public, private
visibility: public
# namespaces created at startup; each entry is created when missing,
# and each entry may set its own visibility (public or private)
initialize:
- name: library
visibility: public
http:
# endpoint can be a domain or domain with port, eg: http://sigma.test.io, https://sigma.test.io:30080, http://127.0.0.1:3000
+5
View File
@@ -91,6 +91,11 @@ namespace:
autoCreate: false
# the automatic created namespace visibility, available: public, private
visibility: public
# namespaces created at startup; each entry is created when missing,
# and each entry may set its own visibility (public or private)
initialize:
- name: library
visibility: public
http:
# endpoint can be a domain or domain with port, eg: http://sigma.test.io, https://sigma.test.io:30080, http://127.0.0.1:3000
+6
View File
@@ -49,6 +49,12 @@ data:
minIdleConns: {{ .Values.external.redis.minIdleConns | default 0 }}
maxRetries: {{ .Values.external.redis.maxRetries | default 0 }}
{{- end }}
namespace:
initialize:
{{- range .Values.config.namespace.initialize }}
- name: {{ .name | quote }}
visibility: {{ .visibility | quote }}
{{- end }}
http:
internalEndpoint: http://{{ include "sigma.distribution" . }}:{{ .Values.service.distribution.port }}
server: 0.0.0.0:{{.Values.service.server.port}}
+6
View File
@@ -154,6 +154,12 @@ config:
redis:
url: ""
#ex: redis://:sigma@localhost:6789/0
namespace:
# namespaces created at startup; each entry is created when missing,
# and each entry may set its own visibility (public or private)
initialize:
- name: library
visibility: public
http:
server: 0.0.0.0:3000
worker: 0.0.0.0:3001
+5
View File
@@ -76,6 +76,11 @@ namespace:
autoCreate: false
# the automatic created namespace visibility, available: public, private
visibility: public
# namespaces created at startup; each entry is created when missing,
# and each entry may set its own visibility (public or private)
initialize:
- name: library
visibility: public
http:
# endpoint can be a domain or domain with port, eg: http://sigma.test.io, https://sigma.test.io:30080, http://127.0.0.1:3000
+5
View File
@@ -80,6 +80,11 @@ namespace:
autoCreate: false
# the automatic created namespace visibility, available: public, private
visibility: public
# namespaces created at startup; each entry is created when missing,
# and each entry may set its own visibility (public or private)
initialize:
- name: library
visibility: public
http:
# endpoint can be a domain or domain with port, eg: http://sigma.test.io, https://sigma.test.io:30080, http://127.0.0.1:3000
+1
View File
@@ -32,6 +32,7 @@ var bootstrapTasks = []bootstrapTask{
{name: "service", run: service.InitDigContainer},
{name: "signing", run: signing},
{name: "user", run: initUser},
{name: "namespace", run: initNamespaces},
}
// Initialize runs all registered bootstrap tasks.
+78
View File
@@ -0,0 +1,78 @@
// Copyright 2026 sigma
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bootstrap
import (
"context"
"errors"
"fmt"
"log/slog"
"go.uber.org/dig"
"gorm.io/gorm"
"github.com/go-sigma/sigma/pkg/config"
"github.com/go-sigma/sigma/pkg/dal/models"
reponamespace "github.com/go-sigma/sigma/pkg/dal/repository/namespace"
"github.com/go-sigma/sigma/pkg/utils/uuid"
)
type initNamespacesParams struct {
dig.In
Config *config.Configuration
NamespaceRepository reponamespace.NamespaceRepository
}
func initNamespaces(digCon *dig.Container) error {
return digCon.Invoke(initNamespacesWithParams)
}
// initNamespacesWithParams ensures every namespace declared under
// namespace.initialize exists. It is idempotent and safe to run on every
// startup, so users can declare any number of namespaces with their own
// visibility.
func initNamespacesWithParams(params initNamespacesParams) error {
ctx := context.Background()
for _, item := range params.Config.Namespace.Initialize {
if err := initNamespace(ctx, params.NamespaceRepository, item); err != nil {
return err
}
}
return nil
}
func initNamespace(ctx context.Context, namespaceRepository reponamespace.NamespaceRepository, item config.ConfigurationNamespaceInit) error {
_, err := namespaceRepository.GetByName(ctx, item.Name)
if err == nil {
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
slog.Error("get namespace failed", "err", err, "namespace", item.Name)
return fmt.Errorf("get namespace %q failed: %w", item.Name, err)
}
err = namespaceRepository.Create(ctx, &models.Namespace{
ID: uuid.NewV7String(),
Name: item.Name,
Visibility: item.Visibility,
})
if err != nil {
slog.Error("create namespace failed", "err", err, "namespace", item.Name)
return fmt.Errorf("create namespace %q failed: %w", item.Name, err)
}
slog.Info("namespace initialized", "namespace", item.Name, "visibility", item.Visibility)
return nil
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2026 sigma
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bootstrap
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/dig"
"go.uber.org/mock/gomock"
"gorm.io/gorm"
"github.com/go-sigma/sigma/pkg/api/enums"
"github.com/go-sigma/sigma/pkg/config"
"github.com/go-sigma/sigma/pkg/dal/models"
reponamespace "github.com/go-sigma/sigma/pkg/dal/repository/namespace"
"github.com/go-sigma/sigma/pkg/logger"
)
func TestInitNamespaces(t *testing.T) {
logger.SetLevel("debug")
tests := []struct {
name string
namespace config.ConfigurationNamespace
genDigCon func(*testing.T, *config.Configuration) *dig.Container
wantErr bool
}{
{
name: "empty",
namespace: config.ConfigurationNamespace{},
genDigCon: func(t *testing.T, _ *config.Configuration) *dig.Container {
ctrl := gomock.NewController(t)
mock := reponamespace.NewMockNamespaceRepository(ctrl)
digCon := dig.New()
require.NoError(t, digCon.Provide(func() reponamespace.NamespaceRepository { return mock }))
return digCon
},
},
{
name: "ensure",
namespace: config.ConfigurationNamespace{
Initialize: []config.ConfigurationNamespaceInit{
{Name: "library", Visibility: enums.VisibilityPublic},
{Name: "internal", Visibility: enums.VisibilityPrivate},
},
},
genDigCon: func(t *testing.T, _ *config.Configuration) *dig.Container {
ctrl := gomock.NewController(t)
mock := reponamespace.NewMockNamespaceRepository(ctrl)
mock.EXPECT().GetByName(gomock.Any(), "library").
Return(&models.Namespace{ID: "namespace-1", Name: "library"}, nil)
mock.EXPECT().GetByName(gomock.Any(), "internal").
Return(nil, gorm.ErrRecordNotFound)
mock.EXPECT().Create(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, namespace *models.Namespace) error {
require.Equal(t, "internal", namespace.Name)
require.Equal(t, enums.VisibilityPrivate, namespace.Visibility)
require.NotEmpty(t, namespace.ID)
return nil
})
digCon := dig.New()
require.NoError(t, digCon.Provide(func() reponamespace.NamespaceRepository { return mock }))
return digCon
},
},
{
name: "get error",
namespace: config.ConfigurationNamespace{
Initialize: []config.ConfigurationNamespaceInit{
{Name: "library", Visibility: enums.VisibilityPublic},
},
},
genDigCon: func(t *testing.T, _ *config.Configuration) *dig.Container {
ctrl := gomock.NewController(t)
mock := reponamespace.NewMockNamespaceRepository(ctrl)
mock.EXPECT().GetByName(gomock.Any(), "library").
Return(nil, gorm.ErrInvalidDB)
digCon := dig.New()
require.NoError(t, digCon.Provide(func() reponamespace.NamespaceRepository { return mock }))
return digCon
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Configuration{Namespace: tt.namespace}
digCon := tt.genDigCon(t, cfg)
require.NoError(t, digCon.Provide(func() *config.Configuration { return cfg }))
err := initNamespaces(digCon)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
+15 -2
View File
@@ -387,10 +387,18 @@ func (c *ConfigurationLocker) WithDefaults() {
}
}
// ConfigurationNamespaceInit declares a namespace created at startup. Users can
// declare any number of namespaces, each with its own visibility.
type ConfigurationNamespaceInit struct {
Name string `yaml:"name"`
Visibility enums.Visibility `yaml:"visibility"`
}
// ConfigurationNamespace controls whether namespaces are created automatically on push and the visibility assigned to them.
type ConfigurationNamespace struct {
AutoCreate bool `yaml:"autoCreate"`
Visibility enums.Visibility `yaml:"visibility"`
AutoCreate bool `yaml:"autoCreate"`
Visibility enums.Visibility `yaml:"visibility"`
Initialize []ConfigurationNamespaceInit `yaml:"initialize"`
}
// WithDefaults applies default values for namespace.
@@ -398,6 +406,11 @@ func (c *ConfigurationNamespace) WithDefaults() {
if c.Visibility.String() == "" {
c.Visibility = enums.VisibilityPrivate
}
for i := range c.Initialize {
if c.Initialize[i].Visibility.String() == "" {
c.Initialize[i].Visibility = c.Visibility
}
}
}
// ConfigurationHttpTLS enables TLS termination for the HTTP server and points at the certificate and private key files.
@@ -368,23 +368,6 @@ CREATE TABLE IF NOT EXISTS `namespace_members` (
CONSTRAINT `namespace_members_unique_with_user_ns_role` UNIQUE (`user_id`, `namespace_id`, `role`, `deleted_at`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
INSERT INTO
`namespaces` (
`id`,
`name`,
`visibility`,
`created_at`,
`updated_at`
)
VALUES
(
'019f56d8-633c-7d64-a115-f793aa8aad22',
'library',
'public',
1775566388880,
1775566388880
);
CREATE TABLE IF NOT EXISTS `webhooks` (
`id` varchar(36) PRIMARY KEY,
`namespace_id` varchar(36),
@@ -426,23 +426,6 @@ CREATE TABLE IF NOT EXISTS "namespace_members" (
CONSTRAINT "namespace_members_unique_with_user_ns_role" UNIQUE ("user_id", "namespace_id", "role", "deleted_at")
);
INSERT INTO
"namespaces" (
"id",
"name",
"visibility",
"created_at",
"updated_at"
)
VALUES
(
'019f56d8-633c-7d64-a115-f793aa8aad22',
'library',
'public',
1775566388880,
1775566388880
);
CREATE TABLE IF NOT EXISTS "webhooks" (
"id" varchar(36) PRIMARY KEY,
"namespace_id" varchar(36),
@@ -372,23 +372,6 @@ CREATE TABLE IF NOT EXISTS `namespace_members` (
CONSTRAINT `namespace_members_unique_with_user_ns_role` UNIQUE (`user_id`, `namespace_id`, `role`, `deleted_at`)
);
INSERT INTO
`namespaces` (
`id`,
`name`,
`visibility`,
`created_at`,
`updated_at`
)
VALUES
(
'019f56d8-633c-7d64-a115-f793aa8aad22',
'library',
'public',
1775566388880,
1775566388880
);
CREATE TABLE IF NOT EXISTS `webhooks` (
`id` varchar(36) PRIMARY KEY,
`namespace_id` varchar(36),
@@ -372,23 +372,6 @@ CREATE TABLE IF NOT EXISTS `namespace_members` (
CONSTRAINT `namespace_members_unique_with_user_ns_role` UNIQUE (`user_id`, `namespace_id`, `role`, `deleted_at`)
);
INSERT INTO
`namespaces` (
`id`,
`name`,
`visibility`,
`created_at`,
`updated_at`
)
VALUES
(
'019f56d8-633c-7d64-a115-f793aa8aad22',
'library',
'public',
1775566388880,
1775566388880
);
CREATE TABLE IF NOT EXISTS `webhooks` (
`id` varchar(36) PRIMARY KEY,
`namespace_id` varchar(36),