Skip to content

Commit 4e959d3

Browse files
authored
feat: add template versioning using tags (#1524)
1 parent f89fec4 commit 4e959d3

File tree

64 files changed

+2643
-369
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2643
-369
lines changed

packages/api/internal/api/api.gen.go

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

packages/api/internal/api/spec.gen.go

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

packages/api/internal/api/types.gen.go

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

packages/api/internal/cache/templates/cache.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package templatecache
22

33
import (
44
"context"
5-
"database/sql"
65
"errors"
76
"fmt"
87
"net/http"
8+
"strings"
99
"time"
1010

1111
"github.com/google/uuid"
@@ -14,8 +14,10 @@ import (
1414
"github.com/e2b-dev/infra/packages/api/internal/api"
1515
"github.com/e2b-dev/infra/packages/api/internal/utils"
1616
sqlcdb "github.com/e2b-dev/infra/packages/db/client"
17+
"github.com/e2b-dev/infra/packages/db/dberrors"
1718
"github.com/e2b-dev/infra/packages/db/queries"
1819
"github.com/e2b-dev/infra/packages/shared/pkg/cache"
20+
"github.com/e2b-dev/infra/packages/shared/pkg/id"
1921
)
2022

2123
const (
@@ -29,6 +31,7 @@ type TemplateInfo struct {
2931
teamID uuid.UUID
3032
clusterID uuid.UUID
3133
build *queries.EnvBuild
34+
tag *string
3235
}
3336

3437
type AliasCache struct {
@@ -72,7 +75,7 @@ func NewTemplateCache(db *sqlcdb.Client) *TemplateCache {
7275
RefreshTimeout: refreshTimeout,
7376
// With this we can use alias for getting template info without having it as a key in the cache
7477
ExtractKeyFunc: func(value *TemplateInfo) string {
75-
return value.template.TemplateID
78+
return buildCacheKey(value.template.TemplateID, value.tag)
7679
},
7780
}
7881
aliasCache := NewAliasCache()
@@ -84,15 +87,25 @@ func NewTemplateCache(db *sqlcdb.Client) *TemplateCache {
8487
}
8588
}
8689

87-
func (c *TemplateCache) Get(ctx context.Context, aliasOrEnvID string, teamID uuid.UUID, clusterID uuid.UUID, public bool) (*api.Template, *queries.EnvBuild, *api.APIError) {
90+
func buildCacheKey(templateID string, tag *string) string {
91+
if tag == nil {
92+
return templateID + ":" + id.DefaultTag
93+
}
94+
95+
return templateID + ":" + *tag
96+
}
97+
98+
func (c *TemplateCache) Get(ctx context.Context, aliasOrEnvID string, tag *string, teamID uuid.UUID, clusterID uuid.UUID, public bool) (*api.Template, *queries.EnvBuild, *api.APIError) {
8899
// Resolve alias to template ID if needed
89100
templateID, found := c.aliasCache.Get(aliasOrEnvID)
90101
if !found {
91102
templateID = aliasOrEnvID
92103
}
93104

105+
cacheKey := buildCacheKey(templateID, tag)
106+
94107
// Fetch or get from cache with automatic refresh
95-
templateInfo, err := c.cache.GetOrSet(ctx, templateID, c.fetchTemplateInfo)
108+
templateInfo, err := c.cache.GetOrSet(ctx, cacheKey, c.fetchTemplateInfo)
96109
if err != nil {
97110
var apiErr *api.APIError
98111
if errors.As(err, &apiErr) {
@@ -115,11 +128,24 @@ func (c *TemplateCache) Get(ctx context.Context, aliasOrEnvID string, teamID uui
115128
}
116129

117130
// fetchTemplateInfo fetches template info from the database
118-
func (c *TemplateCache) fetchTemplateInfo(ctx context.Context, aliasOrEnvID string) (*TemplateInfo, error) {
119-
result, err := c.db.GetTemplateWithBuild(ctx, aliasOrEnvID)
131+
func (c *TemplateCache) fetchTemplateInfo(ctx context.Context, cacheKey string) (*TemplateInfo, error) {
132+
aliasOrEnvID, tag, err := id.ParseTemplateIDOrAliasWithTag(cacheKey)
120133
if err != nil {
121-
if errors.Is(err, sql.ErrNoRows) {
122-
return nil, &api.APIError{Code: http.StatusNotFound, ClientMsg: fmt.Sprintf("template '%s' not found", aliasOrEnvID), Err: err}
134+
return nil, &api.APIError{Code: http.StatusBadRequest, ClientMsg: fmt.Sprintf("invalid template ID: %s", err), Err: err}
135+
}
136+
137+
result, err := c.db.GetTemplateWithBuildByTag(ctx, queries.GetTemplateWithBuildByTagParams{
138+
AliasOrEnvID: aliasOrEnvID,
139+
Tag: tag,
140+
})
141+
if err != nil {
142+
if dberrors.IsNotFoundError(err) {
143+
tagMsg := ""
144+
if tag != nil {
145+
tagMsg = fmt.Sprintf(" with tag '%s'", *tag)
146+
}
147+
148+
return nil, &api.APIError{Code: http.StatusNotFound, ClientMsg: fmt.Sprintf("template '%s'%s not found", aliasOrEnvID, tagMsg), Err: err}
123149
}
124150

125151
return nil, &api.APIError{Code: http.StatusInternalServerError, ClientMsg: fmt.Sprintf("error while getting template: %v", err), Err: err}
@@ -129,7 +155,7 @@ func (c *TemplateCache) fetchTemplateInfo(ctx context.Context, aliasOrEnvID stri
129155
template := result.Env
130156
clusterID := utils.WithClusterFallback(template.ClusterID)
131157

132-
// Update alias cache
158+
// Update alias cache (without tag, as aliases map to template IDs)
133159
c.aliasCache.Set(template.ID, template.ID)
134160
for _, alias := range result.Aliases {
135161
c.aliasCache.Set(alias, template.ID)
@@ -145,12 +171,22 @@ func (c *TemplateCache) fetchTemplateInfo(ctx context.Context, aliasOrEnvID stri
145171
teamID: template.TeamID,
146172
clusterID: clusterID,
147173
build: build,
174+
tag: tag,
148175
}, nil
149176
}
150177

151-
// Invalidate invalidates the cache for the given templateID
152-
func (c *TemplateCache) Invalidate(templateID string) {
153-
c.cache.Delete(templateID)
178+
func (c *TemplateCache) Invalidate(templateID string, tag *string) {
179+
c.cache.Delete(buildCacheKey(templateID, tag))
180+
}
181+
182+
// Invalidate invalidates the cache for the given templateID across all tags
183+
func (c *TemplateCache) InvalidateAllTags(templateID string) {
184+
templateIDKey := templateID + ":"
185+
for _, key := range c.cache.Keys() {
186+
if strings.HasPrefix(key, templateIDKey) {
187+
c.cache.Delete(key)
188+
}
189+
}
154190
}
155191

156192
func (c *TemplateCache) Close(ctx context.Context) error {

packages/api/internal/db/snapshots_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/e2b-dev/infra/packages/db/queries"
1515
"github.com/e2b-dev/infra/packages/db/testutils"
1616
"github.com/e2b-dev/infra/packages/db/types"
17-
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
1817
)
1918

2019
// createTestTeam creates a test team in the database using raw SQL
@@ -85,7 +84,7 @@ func createTestSnapshot(t *testing.T, db *client.Client, teamID uuid.UUID, baseE
8584
},
8685
},
8786
},
88-
OriginNodeID: utils.ToPtr("node-1"),
87+
OriginNodeID: "node-1",
8988
Status: "success",
9089
}
9190

packages/api/internal/handlers/deprecated_template_request_build.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func (a *APIStore) PostTemplatesTemplateID(c *gin.Context, rawTemplateID api.Tem
8585
return
8686
}
8787

88-
templateID, err := id.CleanTemplateID(rawTemplateID)
88+
templateID, _, err := id.ParseTemplateIDOrAliasWithTag(rawTemplateID)
8989
if err != nil {
9090
a.sendAPIStoreError(c, http.StatusBadRequest, fmt.Sprintf("Invalid template ID: %s", rawTemplateID))
9191
telemetry.ReportCriticalError(c.Request.Context(), "invalid template ID", err)
@@ -156,14 +156,44 @@ func (a *APIStore) buildTemplate(
156156
) (*template.RegisterBuildResponse, *api.APIError) {
157157
firecrackerVersion := a.featureFlags.StringFlag(ctx, featureflags.BuildFirecrackerVersion)
158158

159+
var alias *string
160+
var tags []string
161+
162+
if body.Alias != nil {
163+
var err error
164+
a, t, err := id.ParseTemplateIDOrAliasWithTag(*body.Alias)
165+
if err != nil {
166+
return nil, &api.APIError{
167+
Code: http.StatusBadRequest,
168+
ClientMsg: fmt.Sprintf("Invalid alias: %s", err),
169+
Err: err,
170+
}
171+
}
172+
173+
alias = &a
174+
if t != nil {
175+
err = id.ValidateCreateTag(*t)
176+
if err != nil {
177+
return nil, &api.APIError{
178+
Code: http.StatusBadRequest,
179+
ClientMsg: fmt.Sprintf("Invalid tag: %s", err),
180+
Err: err,
181+
}
182+
}
183+
184+
tags = []string{*t}
185+
}
186+
}
187+
159188
// Create the build
160189
data := template.RegisterBuildData{
161190
ClusterID: utils.WithClusterFallback(team.ClusterID),
162191
TemplateID: templateID,
163192
UserID: &userID,
164193
Team: team,
165194
Dockerfile: body.Dockerfile,
166-
Alias: body.Alias,
195+
Alias: alias,
196+
Tags: tags,
167197
StartCmd: body.StartCmd,
168198
ReadyCmd: body.ReadyCmd,
169199
CpuCount: body.CpuCount,

packages/api/internal/handlers/deprecated_template_request_build_v2.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ func (a *APIStore) PostV2Templates(c *gin.Context) {
2323
return
2424
}
2525

26-
t := requestTemplateBuild(ctx, c, a, api.TemplateBuildRequestV3(body))
26+
t := requestTemplateBuild(ctx, c, a, api.TemplateBuildRequestV3{
27+
Names: &[]string{body.Alias},
28+
CpuCount: body.CpuCount,
29+
MemoryMB: body.MemoryMB,
30+
TeamID: body.TeamID,
31+
})
2732
if t != nil {
2833
c.JSON(http.StatusAccepted, &api.TemplateLegacy{
2934
TemplateID: t.TemplateID,

packages/api/internal/handlers/sandbox_create.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,23 +71,24 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
7171

7272
telemetry.ReportEvent(ctx, "Parsed body")
7373

74-
cleanedAliasOrEnvID, err := id.CleanTemplateID(body.TemplateID)
74+
// Parse template ID and optional tag in the format "templateID:tag"
75+
cleanedAliasOrEnvID, tag, err := id.ParseTemplateIDOrAliasWithTag(body.TemplateID)
7576
if err != nil {
76-
a.sendAPIStoreError(c, http.StatusBadRequest, fmt.Sprintf("Invalid environment ID: %s", err))
77+
a.sendAPIStoreError(c, http.StatusBadRequest, fmt.Sprintf("Invalid template ID: %s", err))
7778

78-
telemetry.ReportCriticalError(ctx, "error when cleaning env ID", err)
79+
telemetry.ReportCriticalError(ctx, "error when parsing template ID", err)
7980

8081
return
8182
}
8283

83-
telemetry.ReportEvent(ctx, "Cleaned template ID")
84+
telemetry.ReportEvent(ctx, "Parsed template ID and tag")
8485

8586
_, templateSpan := tracer.Start(ctx, "get-template")
8687
defer templateSpan.End()
8788

8889
// Check if team has access to the environment
8990
clusterID := utils.WithClusterFallback(teamInfo.Team.ClusterID)
90-
env, build, checkErr := a.templateCache.Get(ctx, cleanedAliasOrEnvID, teamInfo.Team.ID, clusterID, true)
91+
env, build, checkErr := a.templateCache.Get(ctx, cleanedAliasOrEnvID, tag, teamInfo.Team.ID, clusterID, true)
9192
if checkErr != nil {
9293
telemetry.ReportCriticalError(ctx, "error when getting template", checkErr.Err)
9394
a.sendAPIStoreError(c, checkErr.Code, checkErr.ClientMsg)

packages/api/internal/handlers/sandbox_kill.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func (a *APIStore) deleteSnapshot(ctx context.Context, sandboxID string, teamID
6666
}
6767
}(context.WithoutCancel(ctx))
6868

69-
a.templateCache.Invalidate(snapshot.TemplateID)
69+
a.templateCache.InvalidateAllTags(snapshot.TemplateID)
7070

7171
return nil
7272
}

0 commit comments

Comments
 (0)