Skip to content

Commit 558103e

Browse files
fix execution of create vm component
Signed-off-by: Emil Todorovski <emil.todorovski@brightmarbles.io>
1 parent 4154f38 commit 558103e

14 files changed

Lines changed: 677 additions & 208 deletions

pkg/core/integration.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ type Integration interface {
8383
HandleRequest(ctx HTTPRequestContext)
8484
}
8585

86+
// OIDCAwareIntegration is an optional interface for integrations that
87+
// require an OIDC provider for authentication (e.g. workload identity
88+
// federation with cloud providers). The registry calls SetOIDCProvider
89+
// once during startup so the integration can lazily initialize
90+
// authenticated clients without waiting for a manual Sync.
91+
type OIDCAwareIntegration interface {
92+
SetOIDCProvider(oidc.Provider)
93+
}
94+
8695
type WebhookHandler interface {
8796

8897
/*

pkg/integrations/azure/actions.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,49 @@ var ImageUbuntu2004LTS = ImageReference{
495495
SKU: "20_04-lts-gen2",
496496
Version: "latest",
497497
}
498+
499+
var ImageUbuntu2204LTS = ImageReference{
500+
Publisher: "Canonical",
501+
Offer: "0001-com-ubuntu-server-jammy",
502+
SKU: "22_04-lts-gen2",
503+
Version: "latest",
504+
}
505+
506+
var ImageUbuntu2404LTS = ImageReference{
507+
Publisher: "Canonical",
508+
Offer: "ubuntu-24_04-lts",
509+
SKU: "server",
510+
Version: "latest",
511+
}
512+
513+
var ImageDebian12 = ImageReference{
514+
Publisher: "Debian",
515+
Offer: "debian-12",
516+
SKU: "12-gen2",
517+
Version: "latest",
518+
}
519+
520+
var ImageWindowsServer2022 = ImageReference{
521+
Publisher: "MicrosoftWindowsServer",
522+
Offer: "WindowsServer",
523+
SKU: "2022-datacenter-g2",
524+
Version: "latest",
525+
}
526+
527+
// ImagePresets maps preset keys to their image references.
528+
var ImagePresets = map[string]ImageReference{
529+
"ubuntu-24.04": ImageUbuntu2404LTS,
530+
"ubuntu-22.04": ImageUbuntu2204LTS,
531+
"ubuntu-20.04": ImageUbuntu2004LTS,
532+
"debian-12": ImageDebian12,
533+
"windows-2022": ImageWindowsServer2022,
534+
}
535+
536+
// ResolveImagePreset returns the ImageReference for a preset key,
537+
// or the Ubuntu 24.04 default if the key is empty or unknown.
538+
func ResolveImagePreset(preset string) ImageReference {
539+
if img, ok := ImagePresets[preset]; ok {
540+
return img
541+
}
542+
return ImageUbuntu2404LTS
543+
}

pkg/integrations/azure/actions_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,27 @@ func TestImageReferenceConstants(t *testing.T) {
191191
assert.Equal(t, "0001-com-ubuntu-server-focal", ImageUbuntu2004LTS.Offer)
192192
assert.Equal(t, "20_04-lts-gen2", ImageUbuntu2004LTS.SKU)
193193
assert.Equal(t, "latest", ImageUbuntu2004LTS.Version)
194+
195+
assert.Equal(t, "Canonical", ImageUbuntu2204LTS.Publisher)
196+
assert.Equal(t, "Canonical", ImageUbuntu2404LTS.Publisher)
197+
assert.Equal(t, "Debian", ImageDebian12.Publisher)
198+
assert.Equal(t, "MicrosoftWindowsServer", ImageWindowsServer2022.Publisher)
199+
}
200+
201+
func TestResolveImagePreset(t *testing.T) {
202+
img := ResolveImagePreset("ubuntu-22.04")
203+
assert.Equal(t, ImageUbuntu2204LTS, img)
204+
205+
img = ResolveImagePreset("windows-2022")
206+
assert.Equal(t, ImageWindowsServer2022, img)
207+
208+
// Unknown preset falls back to Ubuntu 24.04
209+
img = ResolveImagePreset("unknown")
210+
assert.Equal(t, ImageUbuntu2404LTS, img)
211+
212+
// Empty preset falls back to Ubuntu 24.04
213+
img = ResolveImagePreset("")
214+
assert.Equal(t, ImageUbuntu2404LTS, img)
194215
}
195216

196217
func TestImageReference_StructFields(t *testing.T) {

pkg/integrations/azure/arm_client.go

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import (
1414
)
1515

1616
const (
17-
armBaseURL = "https://management.azure.com"
18-
armAPIVersionCompute = "2024-03-01"
19-
armAPIVersionNetwork = "2023-11-01"
20-
armAPIVersionResources = "2024-03-01"
21-
armAPIVersionEventGrid = "2024-06-01-preview"
17+
armBaseURL = "https://management.azure.com"
18+
armAPIVersionCompute = "2024-03-01"
19+
armAPIVersionNetwork = "2023-11-01"
20+
armAPIVersionResources = "2024-03-01"
21+
armAPIVersionEventGrid = "2024-06-01-preview"
22+
armAPIVersionResourceProvider = "2021-04-01"
2223

2324
lroDefaultPollInterval = 5 * time.Second
2425
lroMaxPollDuration = 30 * time.Minute
@@ -83,6 +84,81 @@ func (c *armClient) get(ctx context.Context, url string, dest any) error {
8384
return json.NewDecoder(resp.Body).Decode(dest)
8485
}
8586

87+
// post performs a POST request and returns the raw response.
88+
func (c *armClient) post(ctx context.Context, url string, body io.Reader) (*http.Response, error) {
89+
resp, err := c.doRequest(ctx, http.MethodPost, url, body)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
if resp.StatusCode >= 400 {
95+
defer resp.Body.Close()
96+
return nil, readARMError(resp)
97+
}
98+
99+
return resp, nil
100+
}
101+
102+
// ensureResourceProviderRegistered checks whether an Azure resource provider
103+
// (e.g. "Microsoft.EventGrid") is registered, and if not, attempts to register
104+
// it. The method fails fast if the caller lacks permissions or if registration
105+
// does not complete within a short window.
106+
func (c *armClient) ensureResourceProviderRegistered(ctx context.Context, providerNamespace string) error {
107+
checkURL := fmt.Sprintf("%s/subscriptions/%s/providers/%s?api-version=%s",
108+
armBaseURL, c.subscriptionID, providerNamespace, armAPIVersionResourceProvider)
109+
110+
var provider struct {
111+
RegistrationState string `json:"registrationState"`
112+
}
113+
if err := c.get(ctx, checkURL, &provider); err != nil {
114+
return fmt.Errorf("failed to check %s registration state: %w", providerNamespace, err)
115+
}
116+
117+
if provider.RegistrationState == "Registered" {
118+
return nil
119+
}
120+
121+
// Attempt to register the provider.
122+
registerURL := fmt.Sprintf("%s/subscriptions/%s/providers/%s/register?api-version=%s",
123+
armBaseURL, c.subscriptionID, providerNamespace, armAPIVersionResourceProvider)
124+
125+
resp, err := c.post(ctx, registerURL, nil)
126+
if err != nil {
127+
return fmt.Errorf(
128+
"%s resource provider is not registered and auto-registration failed: %w. "+
129+
"Please register it manually: az provider register --namespace %s",
130+
providerNamespace, err, providerNamespace,
131+
)
132+
}
133+
resp.Body.Close()
134+
135+
// Poll with a short timeout — registration typically takes 10-30s.
136+
deadline := time.Now().Add(2 * time.Minute)
137+
for {
138+
if time.Now().After(deadline) {
139+
return fmt.Errorf(
140+
"timed out waiting for %s to register. "+
141+
"Please register it manually and retry: az provider register --namespace %s",
142+
providerNamespace, providerNamespace,
143+
)
144+
}
145+
146+
select {
147+
case <-ctx.Done():
148+
return ctx.Err()
149+
case <-time.After(5 * time.Second):
150+
}
151+
152+
if err := c.get(ctx, checkURL, &provider); err != nil {
153+
return fmt.Errorf("failed to check registration state: %w", err)
154+
}
155+
156+
if provider.RegistrationState == "Registered" {
157+
return nil
158+
}
159+
}
160+
}
161+
86162
// put performs a PUT request with a JSON body and returns the raw response.
87163
func (c *armClient) put(ctx context.Context, url string, body any) (*http.Response, error) {
88164
jsonBody, err := json.Marshal(body)

pkg/integrations/azure/azure_test.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,66 @@
11
package azure
22

33
import (
4+
"fmt"
45
"net/http"
56
"net/http/httptest"
67
"testing"
8+
"time"
79

10+
"github.com/google/uuid"
811
"github.com/sirupsen/logrus"
912
"github.com/stretchr/testify/assert"
1013
"github.com/stretchr/testify/require"
1114
"github.com/superplanehq/superplane/pkg/configuration"
1215
"github.com/superplanehq/superplane/pkg/core"
16+
"github.com/superplanehq/superplane/pkg/oidc"
1317
)
1418

19+
// mockOIDCProvider implements oidc.Provider for testing.
20+
type mockOIDCProvider struct{}
21+
22+
func (m *mockOIDCProvider) Sign(subject string, duration time.Duration, audience string, additionalClaims map[string]any) (string, error) {
23+
return "mock-jwt-token", nil
24+
}
25+
26+
func (m *mockOIDCProvider) PublicJWKs() []oidc.PublicJWK {
27+
return nil
28+
}
29+
30+
// mockIntegrationContext implements core.IntegrationContext for testing.
31+
type mockIntegrationContext struct {
32+
id string
33+
config map[string]string
34+
}
35+
36+
func (m *mockIntegrationContext) ID() uuid.UUID {
37+
id, _ := uuid.Parse(m.id)
38+
return id
39+
}
40+
41+
func (m *mockIntegrationContext) GetConfig(name string) ([]byte, error) {
42+
if v, ok := m.config[name]; ok {
43+
return []byte(v), nil
44+
}
45+
return nil, fmt.Errorf("config %s not found", name)
46+
}
47+
48+
func (m *mockIntegrationContext) GetMetadata() any { return nil }
49+
func (m *mockIntegrationContext) SetMetadata(any) {}
50+
func (m *mockIntegrationContext) Ready() {}
51+
func (m *mockIntegrationContext) Error(string) {}
52+
func (m *mockIntegrationContext) NewBrowserAction(core.BrowserAction) {}
53+
func (m *mockIntegrationContext) RemoveBrowserAction() {}
54+
func (m *mockIntegrationContext) SetSecret(string, []byte) error { return nil }
55+
func (m *mockIntegrationContext) GetSecrets() ([]core.IntegrationSecret, error) { return nil, nil }
56+
func (m *mockIntegrationContext) RequestWebhook(any) error { return nil }
57+
func (m *mockIntegrationContext) Subscribe(any) (*uuid.UUID, error) { return nil, nil }
58+
func (m *mockIntegrationContext) ScheduleResync(time.Duration) error { return nil }
59+
func (m *mockIntegrationContext) ScheduleActionCall(string, any, time.Duration) error { return nil }
60+
func (m *mockIntegrationContext) ListSubscriptions() ([]core.IntegrationSubscriptionContext, error) {
61+
return nil, nil
62+
}
63+
1564
func TestAzureIntegration_Name(t *testing.T) {
1665
integration := &AzureIntegration{}
1766
assert.Equal(t, "azure", integration.Name())
@@ -189,18 +238,39 @@ func TestAzureIntegration_HandleRequest_Unknown(t *testing.T) {
189238
assert.Equal(t, http.StatusNotFound, rec.Code)
190239
}
191240

192-
func TestAzureIntegration_GetProvider(t *testing.T) {
241+
func TestAzureIntegration_SetOIDCProvider(t *testing.T) {
193242
integration := &AzureIntegration{}
194243

195-
// Initially nil
196-
assert.Nil(t, integration.GetProvider())
244+
assert.Nil(t, integration.oidcProvider)
245+
246+
mockOIDC := &mockOIDCProvider{}
247+
integration.SetOIDCProvider(mockOIDC)
197248

198-
// Set a mock provider
249+
assert.NotNil(t, integration.oidcProvider)
250+
assert.Equal(t, mockOIDC, integration.oidcProvider)
251+
}
252+
253+
func TestAzureIntegration_EnsureProvider_ReturnsCachedProvider(t *testing.T) {
199254
provider := &AzureProvider{}
200-
integration.provider = provider
255+
integration := &AzureIntegration{
256+
provider: provider,
257+
integrationID: "test-id",
258+
}
201259

202-
assert.NotNil(t, integration.GetProvider())
203-
assert.Equal(t, provider, integration.GetProvider())
260+
ctx := &mockIntegrationContext{id: "test-id"}
261+
result, err := integration.ensureProvider(ctx)
262+
assert.NoError(t, err)
263+
assert.Equal(t, provider, result)
264+
}
265+
266+
func TestAzureIntegration_EnsureProvider_FailsWithoutOIDC(t *testing.T) {
267+
integration := &AzureIntegration{}
268+
269+
ctx := &mockIntegrationContext{id: "test-id"}
270+
result, err := integration.ensureProvider(ctx)
271+
assert.Error(t, err)
272+
assert.Nil(t, result)
273+
assert.Contains(t, err.Error(), "OIDC provider not available")
204274
}
205275

206276
func TestConfiguration_Struct(t *testing.T) {

0 commit comments

Comments
 (0)