Skip to content

Commit 74e388e

Browse files
author
ilans
committed
Move Sentinel versioning to organization settings and prepare policy engines for future OPA support
1 parent c1882cd commit 74e388e

24 files changed

Lines changed: 855 additions & 114 deletions

Dockerfile

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,6 @@ ARG TARGETPLATFORM
3232
# STAGE: otfd
3333
# Final stage that takes the `base` stage and the `otfd` binary
3434
FROM base AS otfd
35-
ARG TARGETARCH
36-
ARG SENTINEL_VERSION=0.40.0
37-
RUN --mount=type=cache,target=/etc/apk/cache \
38-
apk add --no-cache curl unzip && \
39-
case "$TARGETARCH" in \
40-
amd64) arch=amd64 ;; \
41-
arm64) arch=arm64 ;; \
42-
*) echo "unsupported TARGETARCH: $TARGETARCH" >&2; exit 1 ;; \
43-
esac && \
44-
curl -fsSLo /tmp/sentinel.zip "https://releases.hashicorp.com/sentinel/${SENTINEL_VERSION}/sentinel_${SENTINEL_VERSION}_linux_${arch}.zip" && \
45-
unzip -d /usr/local/bin /tmp/sentinel.zip && \
46-
chmod +x /usr/local/bin/sentinel && \
47-
rm /tmp/sentinel.zip
4835
COPY $TARGETPLATFORM/otfd /usr/local/bin/
4936
USER otf
5037
ENTRYPOINT ["/usr/local/bin/otfd"]

internal/daemon/daemon.go

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -412,20 +412,21 @@ func New(ctx context.Context, logger logr.Logger, cfg Config) (*Daemon, error) {
412412
RunClient: runService,
413413
})
414414
policyService := policy.NewService(policy.Options{
415-
Logger: logger,
416-
Authorizer: authorizer,
417-
DB: db,
418-
Runs: policyRunAdapter{runs: runService, workspaces: workspaceService},
419-
Workspaces: workspaceService,
420-
Configs: configService,
421-
VCSProviders: vcsService,
422-
RepoHooks: repoService,
423-
States: stateService,
424-
Variables: policyVariableAdapter{variables: variableService},
425-
Plans: policyPlanAdapter{runs: runService},
426-
VCSEventSub: vcsEventBroker,
427-
SentinelPath: cfg.RunnerConfig.SentinelPath,
428-
SentinelWorkDir: cfg.RunnerConfig.SentinelWorkDir,
415+
Logger: logger,
416+
Authorizer: authorizer,
417+
DB: db,
418+
Runs: policyRunAdapter{runs: runService, workspaces: workspaceService},
419+
Organizations: orgService,
420+
Workspaces: workspaceService,
421+
Configs: configService,
422+
VCSProviders: vcsService,
423+
RepoHooks: repoService,
424+
States: stateService,
425+
Variables: policyVariableAdapter{variables: variableService},
426+
Plans: policyPlanAdapter{runs: runService},
427+
VCSEventSub: vcsEventBroker,
428+
PolicyEngineBinDir: cfg.RunnerConfig.PolicyEngineBinDir,
429+
PolicyEngineWorkDir: cfg.RunnerConfig.PolicyEngineWorkDir,
429430
})
430431
runService.SetPolicyService(policyService)
431432
dynamiccredsService, err := dynamiccreds.NewService(dynamiccreds.Options{
@@ -681,12 +682,12 @@ func New(ctx context.Context, logger logr.Logger, cfg Config) (*Daemon, error) {
681682
*user.UserService
682683
*workspace.WorkspaceService
683684
*configversion.ConfigService
684-
}{
685-
RunService: runService,
686-
UserService: userService,
687-
WorkspaceService: workspaceService,
688-
ConfigService: configService,
689-
},
685+
}{
686+
RunService: runService,
687+
UserService: userService,
688+
WorkspaceService: workspaceService,
689+
ConfigService: configService,
690+
},
690691
policyService,
691692
authorizer,
692693
)

internal/integration/organization_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func TestIntegration_Organization(t *testing.T) {
2020
Name: new(uuid.NewString()),
2121
})
2222
require.NoError(t, err)
23+
assert.Equal(t, organization.DefaultSentinelVersion, org.SentinelVersion)
2324

2425
t.Run("duplicate error", func(t *testing.T) {
2526
_, err := daemon.Organizations.CreateOrganization(ctx, organization.CreateOptions{
@@ -56,6 +57,18 @@ func TestIntegration_Organization(t *testing.T) {
5657
assert.Equal(t, want, updated.Name)
5758
})
5859

60+
t.Run("update sentinel version", func(t *testing.T) {
61+
daemon, _, ctx := setup(t, skipDefaultOrganization())
62+
org := daemon.createOrganization(t, ctx)
63+
64+
updated, err := daemon.Organizations.UpdateOrganization(ctx, org.Name, organization.UpdateOptions{
65+
SentinelVersion: new("0.40.0"),
66+
})
67+
require.NoError(t, err)
68+
69+
assert.Equal(t, "0.40.0", updated.SentinelVersion)
70+
})
71+
5972
t.Run("list with pagination", func(t *testing.T) {
6073
daemon, _, ctx := setup(t)
6174
_ = daemon.createOrganization(t, ctx)

internal/integration/organization_ui_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,16 @@ func TestIntegration_OrganizationUI(t *testing.T) {
5959
require.NoError(t, err)
6060
err = page.Locator("input#name").Fill("super-duper-org")
6161
require.NoError(t, err)
62+
err = page.Locator("input#sentinel-version").Fill("0.40.0")
63+
require.NoError(t, err)
6264

63-
err = page.Locator(`//button[text()='Update organization name']`).Click()
65+
err = page.Locator(`//button[text()='Update organization settings']`).Click()
6466
require.NoError(t, err)
6567

6668
err = expect.Locator(page.GetByRole("alert")).ToHaveText("updated organization")
6769
require.NoError(t, err)
70+
err = expect.Locator(page.Locator("input#sentinel-version")).ToHaveValue("0.40.0")
71+
require.NoError(t, err)
6872

6973
// delete the organization
7074
err = page.Locator(`//button[@id='delete-organization-button']`).Click()

internal/organization/db.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ func (db *pgdb) create(ctx context.Context, org *Organization) error {
2525
cost_estimation_enabled,
2626
session_remember,
2727
session_timeout,
28-
allow_force_delete_workspaces
29-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
28+
allow_force_delete_workspaces,
29+
sentinel_version
30+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
3031
org.ID,
3132
org.CreatedAt,
3233
org.UpdatedAt,
@@ -37,6 +38,7 @@ func (db *pgdb) create(ctx context.Context, org *Organization) error {
3738
org.SessionRemember,
3839
org.SessionTimeout,
3940
org.AllowForceDeleteWorkspaces,
41+
org.SentinelVersion,
4042
)
4143
return err
4244
}
@@ -66,15 +68,17 @@ SET
6668
session_remember = $5,
6769
session_timeout = $6,
6870
allow_force_delete_workspaces = $7,
69-
updated_at = $8
70-
WHERE name = $9`,
71+
sentinel_version = $8,
72+
updated_at = $9
73+
WHERE name = $10`,
7174
org.Name,
7275
org.Email,
7376
org.CollaboratorAuthPolicy,
7477
org.CostEstimationEnabled,
7578
org.SessionRemember,
7679
org.SessionTimeout,
7780
org.AllowForceDeleteWorkspaces,
81+
org.SentinelVersion,
7882
org.UpdatedAt,
7983
name,
8084
)

internal/organization/organization.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
package organization
33

44
import (
5+
"fmt"
56
"time"
67

78
"github.com/leg100/otf/internal"
89
"github.com/leg100/otf/internal/resource"
10+
"github.com/leg100/otf/internal/semver"
911
)
1012

1113
const (
@@ -29,6 +31,7 @@ type (
2931
SessionTimeout *int `db:"session_timeout"`
3032
AllowForceDeleteWorkspaces bool `db:"allow_force_delete_workspaces"`
3133
CostEstimationEnabled bool `db:"cost_estimation_enabled"`
34+
SentinelVersion string `db:"sentinel_version"`
3235
}
3336

3437
// UpdateOptions represents the options for updating an organization.
@@ -43,6 +46,7 @@ type (
4346
CollaboratorAuthPolicy *string
4447
CostEstimationEnabled *bool
4548
AllowForceDeleteWorkspaces *bool
49+
SentinelVersion *string
4650
}
4751

4852
// CreateOptions represents the options for creating an organization. See
@@ -58,9 +62,12 @@ type (
5862
SessionRemember *int
5963
SessionTimeout *int
6064
AllowForceDeleteWorkspaces *bool
65+
SentinelVersion *string
6166
}
6267
)
6368

69+
const DefaultSentinelVersion = "latest"
70+
6471
func NewOrganization(opts CreateOptions) (*Organization, error) {
6572
if opts.Name == nil {
6673
return nil, internal.ErrRequiredName
@@ -76,6 +83,7 @@ func NewOrganization(opts CreateOptions) (*Organization, error) {
7683
ID: resource.NewTfeID(resource.OrganizationKind),
7784
Email: opts.Email,
7885
CollaboratorAuthPolicy: opts.CollaboratorAuthPolicy,
86+
SentinelVersion: DefaultSentinelVersion,
7987
}
8088
if opts.SessionTimeout != nil {
8189
org.SessionTimeout = opts.SessionTimeout
@@ -89,6 +97,12 @@ func NewOrganization(opts CreateOptions) (*Organization, error) {
8997
if opts.CostEstimationEnabled != nil {
9098
org.CostEstimationEnabled = *opts.CostEstimationEnabled
9199
}
100+
if opts.SentinelVersion != nil {
101+
if err := ValidateSentinelVersion(*opts.SentinelVersion); err != nil {
102+
return nil, err
103+
}
104+
org.SentinelVersion = *opts.SentinelVersion
105+
}
92106
return &org, nil
93107
}
94108

@@ -118,6 +132,22 @@ func (org *Organization) Update(opts UpdateOptions) error {
118132
if opts.AllowForceDeleteWorkspaces != nil {
119133
org.AllowForceDeleteWorkspaces = *opts.AllowForceDeleteWorkspaces
120134
}
135+
if opts.SentinelVersion != nil {
136+
if err := ValidateSentinelVersion(*opts.SentinelVersion); err != nil {
137+
return err
138+
}
139+
org.SentinelVersion = *opts.SentinelVersion
140+
}
121141
org.UpdatedAt = internal.CurrentTimestamp(nil)
122142
return nil
123143
}
144+
145+
func ValidateSentinelVersion(v string) error {
146+
if v == "latest" {
147+
return nil
148+
}
149+
if !semver.IsValid(v) {
150+
return fmt.Errorf("sentinel version must be a semantic version or 'latest'")
151+
}
152+
return nil
153+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package organization
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestNewOrganizationDefaultsSentinelVersion(t *testing.T) {
11+
org, err := NewOrganization(CreateOptions{
12+
Name: new(t.Name()),
13+
})
14+
require.NoError(t, err)
15+
16+
assert.Equal(t, DefaultSentinelVersion, org.SentinelVersion)
17+
}
18+
19+
func TestNewOrganizationAllowsExplicitSentinelVersion(t *testing.T) {
20+
org, err := NewOrganization(CreateOptions{
21+
Name: new(t.Name()),
22+
SentinelVersion: new("0.40.0"),
23+
})
24+
require.NoError(t, err)
25+
26+
assert.Equal(t, "0.40.0", org.SentinelVersion)
27+
}
28+
29+
func TestNewOrganizationRejectsInvalidSentinelVersion(t *testing.T) {
30+
_, err := NewOrganization(CreateOptions{
31+
Name: new(t.Name()),
32+
SentinelVersion: new("not-a-version"),
33+
})
34+
require.Error(t, err)
35+
assert.Contains(t, err.Error(), "sentinel version")
36+
}
37+
38+
func TestOrganizationUpdateSentinelVersion(t *testing.T) {
39+
org, err := NewOrganization(CreateOptions{
40+
Name: new(t.Name()),
41+
})
42+
require.NoError(t, err)
43+
44+
err = org.Update(UpdateOptions{
45+
SentinelVersion: new("0.40.0"),
46+
})
47+
require.NoError(t, err)
48+
49+
assert.Equal(t, "0.40.0", org.SentinelVersion)
50+
}

internal/organization/ui/handlers.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,18 @@ func (h *Handlers) editOrganization(w http.ResponseWriter, r *http.Request) {
159159

160160
func (h *Handlers) updateOrganization(w http.ResponseWriter, r *http.Request) {
161161
var params struct {
162-
Name organization.Name `schema:"name,required"`
163-
UpdatedName string `schema:"new_name,required"`
162+
Name organization.Name `schema:"name,required"`
163+
UpdatedName string `schema:"new_name,required"`
164+
SentinelVersion string `schema:"sentinel_version"`
164165
}
165166
if err := decode.All(&params, r); err != nil {
166167
helpers.Error(r, w, err.Error(), helpers.WithStatus(http.StatusUnprocessableEntity))
167168
return
168169
}
169170

170171
org, err := h.Organizations.UpdateOrganization(r.Context(), params.Name, organization.UpdateOptions{
171-
Name: &params.UpdatedName,
172+
Name: &params.UpdatedName,
173+
SentinelVersion: &params.SentinelVersion,
172174
})
173175
if err != nil {
174176
helpers.Error(r, w, err.Error())

internal/organization/ui/templates.templ

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ templ organizationEdit(org *organization.Organization) {
7474
<input class="input w-80" type="text" name="new_name" id="name" value={ org.Name.String() } required/>
7575
</div>
7676
<div class="field">
77-
<button class="btn w-72">Update organization name</button>
77+
<label for="sentinel-version">Sentinel version</label>
78+
<input class="input w-80" type="text" name="sentinel_version" id="sentinel-version" value={ org.SentinelVersion } required/>
79+
<span class="description">Use <span class="font-mono">latest</span> or an explicit semantic version such as <span class="font-mono">0.40.0</span>.</span>
80+
</div>
81+
<div class="field">
82+
<button class="btn w-72">Update organization settings</button>
7883
</div>
7984
</form>
8085
<hr class="my-4"/>

internal/organization/ui/templates_templ.go

Lines changed: 22 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)