Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0460d27
feat(ip): implement SGCP DNS alias resource driver
PaulJouvanceau Jul 8, 2026
5283dda
[resfssgcp_nfs] Extract `MockGetAuthInfoProvider` to reusable helper …
Jul 10, 2026
8ee70a8
[testsgcphelper] Relocate package and update imports to reflect new path
cgalibern Jul 10, 2026
51f9ef3
[util/sgcpdnshelper] Introduce `sgcpdnshelper` package with API and i…
cgalibern Jul 10, 2026
dc58b28
[util/sgcpdns] Refactor alias management methods to improve clarity a…
Jul 10, 2026
361ed71
[sgcphelper] Add `AuthInfoFromPath` helper function for retrieving au…
cgalibern Jul 10, 2026
3d410d2
[util/sgcpdnshelper] Fix alias update logic in `main.go` to update in…
cgalibern Jul 10, 2026
880568b
[util/sgcpdnshelper] Add `Counters` struct to track API call statisti…
cgalibern Jul 10, 2026
08659ea
[util/sgcpdnshelper] Add `Search` method to query database for aliase…
cgalibern Jul 10, 2026
33b19ed
[util/sgcpdnshelper] Refactor `DB` methods to use consistent receiver…
cgalibern Jul 10, 2026
6f1ed6a
[util/sgcpdnshelper] Rename package to `sgcpdnstesthelper` to reflect…
cgalibern Jul 10, 2026
ef0dcd7
[resipsgcp_dnsalias] Refactor alias management with dedicated manager…
Jul 10, 2026
838b888
[resipsgcp_dnsalias] Rewrite test suite with streamlined helper funct…
Jul 10, 2026
c0bee9e
[resipsgcp_dnsalias] Add TODO comments for caching and cleanup tasks
cgalibern Jul 10, 2026
0f2a603
[util/sgcpserverfortest] Introduce test server with file and client m…
cgalibern Jul 10, 2026
7d0029e
[util/sgcpserverfortest] Bootstrap DNS alias handler with SGCP
cgalibern Jul 10, 2026
09da3de
[util/sgcp] Rename `Alias` to `CName` in DNS configuration and update…
cgalibern Jul 10, 2026
66655ba
[util/sgcpserverfortest] Rename `Alias` to `CName` in DNS configurati…
cgalibern Jul 10, 2026
8bc1433
[core/driverdb] Add support for `resipsgcp_dnsalias` driver
cgalibern Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/driverdb/drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
_ "github.com/opensvc/om3/v3/drivers/resfszfs"
_ "github.com/opensvc/om3/v3/drivers/resiphost"
_ "github.com/opensvc/om3/v3/drivers/resiproute"
_ "github.com/opensvc/om3/v3/drivers/resipsgcp_dnsalias"
_ "github.com/opensvc/om3/v3/drivers/ressharenfs"
_ "github.com/opensvc/om3/v3/drivers/ressyncrsync"
_ "github.com/opensvc/om3/v3/drivers/ressyncsymsnapvx"
Expand Down
48 changes: 10 additions & 38 deletions drivers/resfssgcp_nfs/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"net/http/httptest"
"testing"

"github.com/opensvc/om3/v3/drivers/sgcpauthtesthelper"
"github.com/opensvc/om3/v3/util/testsgcphelper"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/opensvc/om3/v3/core/driver"
"github.com/opensvc/om3/v3/core/resource"
"github.com/opensvc/om3/v3/core/status"
"github.com/opensvc/om3/v3/util/sgcp"
"github.com/opensvc/om3/v3/util/sgcp/testsgcphelper"
)

func newDrvWithRid(s string) *T {
Expand All @@ -25,40 +27,10 @@ func newDrvWithRid(s string) *T {
return d
}

type (
tAuthInfo struct {
sgcp.AuthInfo
}
)

func (a *tAuthInfo) GetAuthInfo(string) (*sgcp.AuthInfo, error) {
return &a.AuthInfo, nil
}

func tCreateAuthInfo(s string) *tAuthInfo {
var a sgcp.AuthInfo
switch s {
case "id1":
a = sgcp.AuthInfo{
AccountID: "account_1",
ClientID: "client_id_1",
ClientSecret: "client_secret_1",
Signature: "1",
}
default:
a = sgcp.AuthInfo{
AccountID: "account_default",
ClientID: "client_id_efault",
ClientSecret: "client_secret_default",
Signature: "default",
}
}
return &tAuthInfo{AuthInfo: a}
}

// Setup initializes the test environment by configuring SGCP with a temporary configuration file.
// It returns a cleanup function that resets the configuration to a null state when invoked.
func Setup(t *testing.T) func() {
t.Helper()
cfgFile := testsgcphelper.InstallConfig(t)
sgcp.SetConfigForTest(cfgFile)
require.NotNil(t, sgcp.GetConfig())
Expand Down Expand Up @@ -89,7 +61,7 @@ func TestConfigure(t *testing.T) {
defer Setup(t)()

drv := newDrvWithRid("test-rid")
drv.authInfoer = tCreateAuthInfo("id1")
drv.authInfoer = sgcpauthtesthelper.NewMockGetAuthInfoProvider("id1")
// Set configuration
drv.UUID = "test-uuid"
drv.Host = "test-host"
Expand All @@ -111,7 +83,7 @@ func TestConfigureWhenNoConfig(t *testing.T) {
sgcp.SetConfigForTest("")

drv := newDrvWithRid("test-rid")
drv.authInfoer = tCreateAuthInfo("id1")
drv.authInfoer = sgcpauthtesthelper.NewMockGetAuthInfoProvider("id1")
// Set configuration
drv.UUID = "test-uuid"
drv.Host = "test-host"
Expand All @@ -126,7 +98,7 @@ func TestConfigureWithMissingUUID(t *testing.T) {
defer Setup(t)()

drv := newDrvWithRid("test-rid")
drv.authInfoer = tCreateAuthInfo("id1")
drv.authInfoer = sgcpauthtesthelper.NewMockGetAuthInfoProvider("id1")

// This should still work since UUID is set via the configuration
drv.UUID = "test-uuid"
Expand All @@ -139,7 +111,7 @@ func TestIsClientIgnored(t *testing.T) {
defer Setup(t)()

drv := newDrvWithRid("test-rid")
drv.authInfoer = tCreateAuthInfo("id1")
drv.authInfoer = sgcpauthtesthelper.NewMockGetAuthInfoProvider("id1")

err := drv.Configure()
assert.NoError(t, err)
Expand Down Expand Up @@ -173,7 +145,7 @@ func TestGetNFSClients(t *testing.T) {
defer Setup(t)()

drv := newDrvWithRid("test-rid")
drv.authInfoer = tCreateAuthInfo("id1")
drv.authInfoer = sgcpauthtesthelper.NewMockGetAuthInfoProvider("id1")
require.NoError(t, drv.Configure())

// Set up ignored hosts
Expand Down Expand Up @@ -303,7 +275,7 @@ func TestLabel(t *testing.T) {
defer Setup(t)()

drv := newDrvWithRid("test-rid")
drv.authInfoer = tCreateAuthInfo("id1")
drv.authInfoer = sgcpauthtesthelper.NewMockGetAuthInfoProvider("id1")

drv.UUID = "test-uuid"
drv.MountPoint = "/tmp/foo"
Expand Down
188 changes: 184 additions & 4 deletions drivers/resipsgcp_dnsalias/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,205 @@ package resipsgcp_dnsalias

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/opensvc/om3/v3/core/datarecv"
"github.com/opensvc/om3/v3/core/resource"
"github.com/opensvc/om3/v3/core/status"
"github.com/opensvc/om3/v3/drivers/sgcphelper"
"github.com/opensvc/om3/v3/util/hostname"
"github.com/opensvc/om3/v3/util/httpclientcache"
"github.com/opensvc/om3/v3/util/plog"
"github.com/opensvc/om3/v3/util/sgcp"
)

const noneTarget = "none."

type (
T struct {
resource.T
resource.Restart
datarecv.DataRecv

UUID string `json:"uuid,omitempty"`
Name string `json:"name,omitempty"`
Target string `json:"target,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
Secret string `json:"secret,omitempty"`
Endpoint string `json:"endpoint,omitempty"`

mgr *mgr

// for tests
api apiProvider
}

mgr struct {
alias alias
api apiProvider
log *plog.Logger
}

// alias decoupled from sgcp.Alias to allow for future changes
alias struct {
UUID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Target string `json:"target,omitempty"`
FQDN string `json:"fqdn,omitempty"`
ZoneID string `json:"zone_id,omitempty"`
}

aliasListResponse struct {
Aliases []sgcp.Alias `json:"aliases"`
}

apiProvider interface {
CheckStatusCode(method string, url string, got int, wanted ...int) error
CreateAlias(ctx context.Context, zoneID, name, target string) (alias *sgcp.Alias, err error)
DeleteAlias(ctx context.Context, zoneID, aliasUUID string) (err error)
GetAliases(ctx context.Context, zoneID, name, uuid string) (method, url string, code int, data []byte, err error)
UpdateAlias(ctx context.Context, zoneID string, aliasUUID string, name string, target string) (alias *sgcp.Alias, err error)
}

authInfoProvider interface {
GetAuthInfo(string) (*sgcp.AuthInfo, error)
}
)

// New creates a new SGCP NFS filesystem resource driver
func New() resource.Driver {
return &T{}
}

func (t *T) Configure() error {
cfg := sgcp.GetConfig()
if cfg == nil {
return fmt.Errorf("mandatory sgcp config file is required: %s", sgcp.DefaultConfigPath)
}
if t.Target == "" {
t.Target = hostname.Hostname()
}

// secret is mandatory: define from keyword, fallback to cfg default, ensure not empty
if t.Secret == "" {
t.Secret = cfg.GetDefaultSecret()
}
if t.Secret == "" {
return errors.New("secret is required")
}

// endpoint is mandatory: define from keyword, fallback to cfg default, ensure not empty
if t.Endpoint == "" {
t.Endpoint = cfg.DNS.BaseURL
}
if t.Endpoint == "" {
return errors.New("endpoint is required")
}

// zoneid is mandatory
if t.ZoneID == "" {
return errors.New("zone_id is required")
}
if t.UUID == "" && t.Name == "" {
return errors.New("alias need define at least name or uuid")
}

return t.configureMgr(cfg)
}

func (t *T) configureMgr(cfg *sgcp.Config) error {
mgr := &mgr{
alias: alias{UUID: t.UUID, Name: t.Name, Target: t.Target, ZoneID: t.ZoneID},
log: t.Log(),
}
if t.api != nil {
// allow custom api for tests
mgr.api = t.api
t.mgr = mgr
return nil
}

httpClient, err := httpclientcache.Client(httpclientcache.Options{Timeout: 30 * time.Second})
if err != nil {
return fmt.Errorf("failed to create http client: %w", err)
}

authInfo, err := sgcphelper.AuthInfoFromPath(t.Secret)
if err != nil {
return fmt.Errorf("get auth info: %w", err)
}

tokenFactory := sgcp.NewTokenFactory(t.Log(), httpClient, &cfg.Auth, authInfo)

if t.api != nil {
mgr.api = t.api
} else {
mgr.api = sgcp.NewDNSAPI(cfg, httpClient, t.Log(), tokenFactory)
}

t.mgr = mgr
return nil
}

func (t *T) Start(ctx context.Context) error {
// TODO: implement cache cleanup
return t.mgr.createOrUpdate(ctx, t.Target)
}

func (t *T) Stop(ctx context.Context) error {
// TODO: implement cache cleanup
if t.UUID != "" {
return t.mgr.createOrUpdate(ctx, noneTarget)
}
return t.mgr.delete(ctx)
}

func (t *T) Status(ctx context.Context) status.T {
return status.NotApplicable
// TODO: implement cache cleanup if command is not called from the scheduler
aliases, err := t.mgr.getAliases(ctx)
if err != nil {
t.StatusLog().Error("get alias failed: %s", err)
}

if len(aliases) == 0 {
t.StatusLog().Info("not found")
return status.Down
}
if len(aliases) > 1 {
t.StatusLog().Warn("found multiple aliases: %v", aliases)
return status.Warn
}
found := aliases[0]
if found.Target == noneTarget || found.Target == strings.TrimSuffix(noneTarget, ".") {
t.StatusLog().Info("alias target is disabled")
return status.Down
}
if t.Name != "" && found.Name != t.Name {
t.StatusLog().Warn("alias name mismatch: found %s instead of %s", found.Name, t.Name)
return status.Warn
}
if found.Target != t.Target {
t.StatusLog().Info("alias target %s != %s", found.Target, t.Target)
return status.Down
}
if t.UUID != "" && found.UUID != "" && found.UUID != t.UUID {
t.StatusLog().Warn("alias uuid mismatch: found %s instead of %s", found.UUID, t.UUID)
return status.Warn
}
t.StatusLog().Info("%s => %s", found.FQDN, found.Target)
return status.Up
}

func (t *T) Label(context.Context) string {
if t.Name != "" {
return t.Name
}
if t.UUID != "" {
return t.UUID
}
return t.ZoneID
}

func (t *T) Boot(ctx context.Context) error {
return t.Stop(ctx)
}
Loading
Loading