Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions plugin/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
package plugin

import (
"fmt"
"os"
"strconv"
"strings"
)

const (
resourceManagerGroup = "resourcemanager.miloapis.com"
miloAPIVersion = "v1alpha1"
)

// PluginContext holds the context injected by datumctl before exec-replacing a plugin.
Expand Down Expand Up @@ -42,3 +49,40 @@ func Context() PluginContext {
Session: os.Getenv("DATUM_SESSION"),
}
}

// ControlPlaneURL returns the scoped Datum Cloud control-plane base URL for this
// context. When Project is set it returns the project control plane; otherwise it
// returns the organization control plane. The APIHost is normalized to include an
// https:// scheme if it has none.
func (c PluginContext) ControlPlaneURL() (string, error) {
if c.APIHost == "" {
return "", fmt.Errorf("DATUM_API_HOST is not set; is this plugin running via datumctl?")
}
base := normalizeAPIHost(c.APIHost)

switch {
case c.Project != "":
return fmt.Sprintf("%s/apis/%s/%s/projects/%s/control-plane",
base, resourceManagerGroup, miloAPIVersion, c.Project), nil
case c.Org != "":
return fmt.Sprintf("%s/apis/%s/%s/organizations/%s/control-plane",
base, resourceManagerGroup, miloAPIVersion, c.Org), nil
default:
return "", fmt.Errorf("no Datum Cloud org or project in context; set DATUM_ORG or DATUM_PROJECT")
}
}

// ControlPlaneURL is shorthand for Context().ControlPlaneURL().
func ControlPlaneURL() (string, error) {
return Context().ControlPlaneURL()
}

// normalizeAPIHost adds an https:// scheme if the host has none and strips any
// trailing slashes. This deliberately duplicates internal/datumconfig's
// EnsureScheme and CleanBaseServer: this package must not import internal.
func normalizeAPIHost(host string) string {
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
host = "https://" + host
}
return strings.TrimRight(host, "/")
}
96 changes: 96 additions & 0 deletions plugin/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,99 @@ func TestContext_apiVersionParseError(t *testing.T) {
t.Errorf("PluginAPIVersion = %d, want 0 for non-numeric input", ctx.PluginAPIVersion)
}
}

// TestPluginContext_ControlPlaneURL verifies scope selection (project wins over
// org), API host scheme normalization, and the error cases for an incomplete
// context.
func TestPluginContext_ControlPlaneURL(t *testing.T) {
t.Parallel()

const (
projectPrefix = "/apis/resourcemanager.miloapis.com/v1alpha1/projects/"
orgPrefix = "/apis/resourcemanager.miloapis.com/v1alpha1/organizations/"
)

tests := []struct {
name string
ctx PluginContext
want string
wantErr bool
}{
{
name: "project wins over org",
ctx: PluginContext{APIHost: "api.datum.net", Org: "my-org", Project: "my-project"},
want: "https://api.datum.net" + projectPrefix + "my-project/control-plane",
},
{
name: "org only",
ctx: PluginContext{APIHost: "api.datum.net", Org: "my-org"},
want: "https://api.datum.net" + orgPrefix + "my-org/control-plane",
},
{
name: "https scheme already present",
ctx: PluginContext{APIHost: "https://api.datum.net", Project: "my-project"},
want: "https://api.datum.net" + projectPrefix + "my-project/control-plane",
},
{
name: "http scheme preserved",
ctx: PluginContext{APIHost: "http://localhost:8080", Project: "my-project"},
want: "http://localhost:8080" + projectPrefix + "my-project/control-plane",
},
{
name: "trailing slash trimmed",
ctx: PluginContext{APIHost: "https://api.datum.net/", Org: "my-org"},
want: "https://api.datum.net" + orgPrefix + "my-org/control-plane",
},
{
name: "missing api host",
ctx: PluginContext{Org: "my-org", Project: "my-project"},
wantErr: true,
},
{
name: "missing org and project",
ctx: PluginContext{APIHost: "api.datum.net"},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

got, err := tt.ctx.ControlPlaneURL()
if tt.wantErr {
if err == nil {
t.Fatalf("ControlPlaneURL() = %q, want error", got)
}
if got != "" {
t.Errorf("ControlPlaneURL() = %q on error, want empty", got)
}
return
}
if err != nil {
t.Fatalf("ControlPlaneURL() error = %v, want nil", err)
}
if got != tt.want {
t.Errorf("ControlPlaneURL() = %q, want %q", got, tt.want)
}
})
}
}

// TestControlPlaneURL_packageLevel verifies that the package-level
// ControlPlaneURL() reads the injected environment.
func TestControlPlaneURL_packageLevel(t *testing.T) {
// Not parallel — uses t.Setenv.
t.Setenv("DATUM_API_HOST", "api.test.datum.net")
t.Setenv("DATUM_ORG", "test-org")
t.Setenv("DATUM_PROJECT", "test-project")

got, err := ControlPlaneURL()
if err != nil {
t.Fatalf("ControlPlaneURL() error = %v, want nil", err)
}
want := "https://api.test.datum.net/apis/resourcemanager.miloapis.com/v1alpha1/projects/test-project/control-plane"
if got != want {
t.Errorf("ControlPlaneURL() = %q, want %q", got, want)
}
}